LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 178 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/8
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/11
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageClustering and PCACore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/24
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernanceData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/19
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnProduction ML SystemsFeature Engineering for Production ML
⚙️MediumMLOps & Deployment

Feature Engineering for Production ML

Turn training-job events into stable prediction inputs while preventing leakage and training-serving mismatch.

14 min read
Learning path
Step 42 of 178 in the full curriculum
Dataset Pipelines and Data QualityBatch and Streaming Feature Pipelines

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

At 09:00 on May 1, 2026 the scheduler asks a blunt question: will training job J-204 miss its SLA? The scoring service doesn't read the raw event log. It needs one frozen row of measurements that already existed at that moment.

A versioned dataset with honest splits still stops one step short of that row. Schema checks keep columns typed. Grouped splits keep related records from leaking into evaluation. Those checks don't freeze what each number meant at prediction time.

Those numbers are features. hours_since_last_heartbeat compresses many scheduler pings into one input. Inventing a column is cheap. Making the same column mean the same thing in a training snapshot and in a live request is the actual work.

Start with a prediction timestamp

At that 09:00 ask, only some fields on the job record are legal inputs. Heartbeats after the timestamp aren't available to the prediction service, so they can't appear in the training row either.

Candidate fieldKnown at prediction time?Use as feature?
runner poolyesyes, categorical
queued minutesyesyes, numeric
hours since most recent heartbeatyesyes, numeric
cluster queue backlogyesyes, numeric
finished timestampnono, it defines the eventual label
post-SLA escalationnono, it leaks the outcome

The label may be computed later as missed_sla = 1. A feature must be computed from history ending at the prediction timestamp. If a training row contains the post-SLA escalation, feature leakage rewards the model for reading the answer.

That leak isn't subtle in a warehouse dump that already has the later escalation bit:

leaky-escalation-rule.py
1rows = [ 2 {"missed_sla": 1, "escalation": True, "queue_backlog": 18}, 3 {"missed_sla": 0, "escalation": False, "queue_backlog": 3}, 4 {"missed_sla": 1, "escalation": True, "queue_backlog": 9}, 5 {"missed_sla": 0, "escalation": False, "queue_backlog": 22}, 6] 7 8def accuracy(predict): 9 hits = sum(predict(row) == row["missed_sla"] for row in rows) 10 return hits / len(rows) 11 12print("escalation rule:", accuracy(lambda row: int(row["escalation"]))) 13print("backlog > 10:", accuracy(lambda row: int(row["queue_backlog"] > 10)))
Output
1escalation rule: 1.0 2backlog > 10: 0.5

On this four-row dump, predicting a miss exactly when the row was escalated is perfect. queue_backlog > 10 is a coin flip. At 09:00 the escalation field doesn't exist yet, so the perfect rule can't run. The backlog number can.

Sculley et al. treat data dependencies, hidden feedback loops, undeclared consumers, and configuration as ML-specific technical debt.[1]Reference 1Hidden Technical Debt in Machine Learning Systems.https://research.google/pubs/hidden-technical-debt-in-machine-learning-systems/ Feature definitions sit in that graph. When the time boundary is implicit, an impressive offline score doesn't survive serving.

Work the cutoff by hand before writing a transform. Prediction time is 09:00. Heartbeats exist at 01:00, 08:30, and 12:00. Only the first two are visible. The latest visible ping is 08:30, so heartbeat age is 30 minutes, which is 0.5 hours. The noon ping would make the age negative. Don't cap a future event to zero. Drop it.

Job J-204 cutoff at 09:00: heartbeats at 01:00 and 08:30 stay in the known panel and compress to a 0.5-hour heartbeat age, while the 12:00 heartbeat, May 3 finished_at, and post-SLA escalation sit in the future panel outside the emitted row [a100-pool, 42.0, 0.5, 18.0, 0, 0].
The 09:00 divider is the contract. Heartbeats at 01:00 and 08:30 stay in; hb_age_h is hours since the 08:30 ping (0.5). Short headers map to queued_minutes, hours_since_last_heartbeat, and queue_backlog_missing. The noon heartbeat, May 3 finish, and post-SLA escalation stay out of the emitted row.
Diagram showing Job events through 09:00, Feature contract types + missing policy, Training snapshot label joins later, and Online request same computation.
Job events through 09:00, Feature contract types + missing policy, Training snapshot label joins later, and Online request same computation.

The shared contract is the only reason the training snapshot and the live request can claim to mean the same thing. Labels join later. They never travel with the model input.

Why must a feature definition begin with a prediction timestamp?

Answer

The timestamp defines which source facts were available when the prediction was made. Without it, a training row can silently include future outcomes or later updates that live serving couldn't access.

Write the missing-value and category rules

A cutoff still isn't a model input. Each surviving field needs a type, a missing rule, and a reason to exist. That's the feature contract for this job-SLA scorer:

FeatureTypeMissing ruleWhy it can help
queued_minutesnumericreject if absent; allow 0longer queue waits expose more scheduling risk
hours_since_last_heartbeatnumericcap at 180stale heartbeats signal job risk
queue_backlognumericuse measured queue onlyqueue pressure affects scheduling and preemption
runner_poolcategoricalreject if absent; map unseen to otherrunner pools have different networks
priority_jobbooleandefault false only when source guarantees itpriority changes SLA and retry policy

A missing value is a product decision. Filling missing queue_backlog with zero says "unknown congestion means no congestion," which is rarely defensible. Store an extra queue_backlog_missing indicator, or stop scoring until the feed recovers.

Categorical values need a policy too. If a new runner pool appears after training, the online encoder can't invent a new model column. An other bucket stays stable while a new model candidate is evaluated. Missing runner-pool data is different: it may signal a broken source feed, so don't silently fold it into other.

The 180-hour heartbeat cap is part of the fitted contract, not a live knob. Sculley et al. call this CACE, Changing Anything Changes Everything: raising the cap at deploy time without retraining means every weight that reads heartbeat age is reading a different number.[1]Reference 1Hidden Technical Debt in Machine Learning Systems.https://research.google/pubs/hidden-technical-debt-in-machine-learning-systems/

Keep only events at or before 09:00

Make the prediction timestamp explicit, then drop later events before aggregating. Keep heartbeats at or before 09:00. Treat a heartbeat as visible as soon as it occurs. The next lesson adds a separate ingestion timestamp so a replay can't use an earlier heartbeat that arrived late.

keep-visible-events.py
1from datetime import datetime 2 3prediction_time = datetime.fromisoformat("2026-05-01T09:00:00") 4heartbeats = [ 5 datetime.fromisoformat("2026-05-01T01:00:00"), 6 datetime.fromisoformat("2026-05-01T08:30:00"), 7 datetime.fromisoformat("2026-05-01T12:00:00"), 8] 9 10visible_heartbeats = [heartbeat for heartbeat in heartbeats if heartbeat <= prediction_time] 11print("visible heartbeats:", len(visible_heartbeats)) 12print("latest visible:", max(visible_heartbeats).isoformat()) 13print("heartbeat age hours:", (prediction_time - max(visible_heartbeats)).total_seconds() / 3600)
Output
1visible heartbeats: 2 2latest visible: 2026-05-01T08:30:00 3heartbeat age hours: 0.5

Filtering source history isn't enough by itself. The row contract must also enforce output keys, types, missing-value meaning, fitted category mappings, and numeric bounds:

  • queue_backlog=0 means a measured empty queue; missing backlog emits queue_backlog_missing=1.
  • an unseen but valid runner pool maps to fitted category other; an absent pool is rejected.
  • heartbeat age is capped at the fitted 180-hour limit, while a future heartbeat is rejected.
  • an output allowlist blocks future-only keys such as finished_at.

Source filtering proves values were available at the cutoff. The allowlist proves only declared keys leave the transform. Neither check replaces offline/online parity.

Assemble the contract in code

The source record may contain post-job-SLA fields because labels need them later. The returned model row may contain only allowlisted feature keys.

build-feature-row.py
1from datetime import datetime 2from math import isfinite 3 4allowed_feature_keys = { 5 "runner_pool", 6 "queued_minutes", 7 "hours_since_last_heartbeat", 8 "queue_backlog", 9 "queue_backlog_missing", 10 "priority_job", 11} 12known_runner_pools = {"a100-pool", "h100-pool"} 13future_only_keys = {"finished_at", "post_sla_escalation"} 14 15def make_features(row, prediction_time): 16 required_keys = {"queued_minutes", "last_heartbeat_at", "priority_job"} 17 missing_keys = sorted(required_keys - row.keys()) 18 if missing_keys: 19 raise ValueError(f"missing required fields: {missing_keys}") 20 21 queued_minutes = float(row["queued_minutes"]) 22 if not isfinite(queued_minutes) or queued_minutes < 0: 23 raise ValueError("queued_minutes must be finite and nonnegative") 24 25 runner_pool = row.get("runner_pool") 26 if not isinstance(runner_pool, str) or not runner_pool.strip(): 27 raise ValueError("runner_pool is missing") 28 runner_pool = runner_pool.strip().casefold() 29 30 if not isinstance(row["priority_job"], bool): 31 raise ValueError("priority_job must be boolean") 32 33 last_heartbeat = datetime.fromisoformat(row["last_heartbeat_at"]) 34 if last_heartbeat > prediction_time: 35 raise ValueError("last_heartbeat_at is after prediction_time") 36 heartbeat_age = min((prediction_time - last_heartbeat).total_seconds() / 3600, 180.0) 37 38 queue_backlog = row.get("queue_backlog") 39 if queue_backlog is not None: 40 queue_backlog = float(queue_backlog) 41 if not isfinite(queue_backlog) or queue_backlog < 0: 42 raise ValueError("queue_backlog must be finite and nonnegative") 43 features = { 44 "runner_pool": runner_pool if runner_pool in known_runner_pools else "other", 45 "queued_minutes": queued_minutes, 46 "hours_since_last_heartbeat": heartbeat_age, 47 "queue_backlog": 0.0 if queue_backlog is None else queue_backlog, 48 "queue_backlog_missing": int(queue_backlog is None), 49 "priority_job": int(row["priority_job"]), 50 } 51 52 blocked_fields = sorted(features.keys() - allowed_feature_keys) 53 if blocked_fields: 54 raise ValueError(f"unexpected feature fields: {blocked_fields}") 55 return features 56 57prediction_time = datetime.fromisoformat("2026-05-01T09:00:00") 58job = { 59 "job_id": "J-204", 60 "runner_pool": "a100-pool", 61 "queued_minutes": 42, 62 "queue_backlog": 18, 63 "priority_job": False, 64 "last_heartbeat_at": "2026-05-01T08:30:00", 65 "finished_at": "2026-05-03T14:00:00", 66 "post_sla_escalation": True, 67} 68 69features = make_features(job, prediction_time) 70print(features) 71print("future fields leaked:", sorted(features.keys() & future_only_keys))
Output
1{'runner_pool': 'a100-pool', 'queued_minutes': 42.0, 'hours_since_last_heartbeat': 0.5, 'queue_backlog': 18.0, 'queue_backlog_missing': 0, 'priority_job': 0} 2future fields leaked: []

finished_at and post_sla_escalation stay on the source record for a later label join. They never appear in features. Probe the highest-risk branches next: missing backlog, unseen pool, absent pool, future heartbeat, and a negative queue time.

preserve-missing-backlog.py
1missing_backlog = make_features({**job, "queue_backlog": None}, prediction_time) 2assert missing_backlog["queue_backlog"] == 0.0 3assert missing_backlog["queue_backlog_missing"] == 1
map-unseen-runner-pool.py
1unseen_pool = make_features({**job, "runner_pool": "l4-pool"}, prediction_time) 2assert unseen_pool["runner_pool"] == "other"
reject-missing-runner-pool.py
1try: 2 make_features({**job, "runner_pool": None}, prediction_time) 3except ValueError as error: 4 assert str(error) == "runner_pool is missing" 5else: 6 raise AssertionError("missing runner_pool was accepted")
reject-future-heartbeat.py
1try: 2 make_features({**job, "last_heartbeat_at": "2026-05-01T12:00:00"}, prediction_time) 3except ValueError as error: 4 assert str(error) == "last_heartbeat_at is after prediction_time" 5else: 6 raise AssertionError("future heartbeat was accepted")
accept-zero-queue-time.py
1zero_queue = make_features({**job, "queued_minutes": 0}, prediction_time) 2assert zero_queue["queued_minutes"] == 0.0
reject-negative-queue-time.py
1try: 2 make_features({**job, "queued_minutes": -1}, prediction_time) 3except ValueError as error: 4 assert str(error) == "queued_minutes must be finite and nonnegative" 5else: 6 raise AssertionError("negative queued_minutes was accepted")

A model still needs a fixed vector order and fitted categorical mapping. Version both beside the model artifact. For J-204 at 09:00 that vector is the row the figure emitted:

vectorize-features.py
1runner_pool_code = {"a100-pool": 0, "h100-pool": 1, "other": 2} 2feature_order = ( 3 "runner_pool_code", 4 "queued_minutes", 5 "hours_since_last_heartbeat", 6 "queue_backlog", 7 "queue_backlog_missing", 8 "priority_job", 9) 10features = { 11 "runner_pool": "a100-pool", 12 "queued_minutes": 42.0, 13 "hours_since_last_heartbeat": 0.5, 14 "queue_backlog": 18.0, 15 "queue_backlog_missing": 0, 16 "priority_job": 0, 17} 18 19vector = [ 20 runner_pool_code[features["runner_pool"]], 21 features["queued_minutes"], 22 features["hours_since_last_heartbeat"], 23 features["queue_backlog"], 24 features["queue_backlog_missing"], 25 features["priority_job"], 26] 27print("order:", feature_order) 28print("vector:", vector)
Output
1order: ('runner_pool_code', 'queued_minutes', 'hours_since_last_heartbeat', 'queue_backlog', 'queue_backlog_missing', 'priority_job') 2vector: [0, 42.0, 0.5, 18.0, 0, 0]

Same names in a different order, or a regenerated runner_pool_code map, would feed the trained weights a different story. The package has to carry the fitted mapping, the 180-hour cap, and this order with the weights.

Why keep both queue_backlog=0 and queue_backlog_missing=1 when the source value is absent?

Answer

Measured zero and unknown congestion aren't the same fact. The indicator preserves that distinction instead of teaching the model that a broken feed means an empty queue.

Changing one fitted number changes the vector

The 180-hour heartbeat cap is part of the trained package. Sculley et al. call the failure CACE: Changing Anything Changes Everything.[1]Reference 1Hidden Technical Debt in Machine Learning Systems.https://research.google/pubs/hidden-technical-debt-in-machine-learning-systems/ Job J-188 last heartbeated 200 hours before scoring. Training clipped that age to 180.0. A later deploy that "just" raises the cap to 360 hours writes 200.0 into the same slot. Every weight that reads heartbeat age now sees a number the fit never produced.

Same 200-hour heartbeat age under two caps. The training package used a 180-hour cap, so the emitted hb_age_h is 180.0. A live service that raises the cap to 360 without retraining emits 200.0 in the same vector slot, so every weight that reads heartbeat age sees a different number.
The raw age is the same 200 hours. Training stored 180.0 because that was the fitted cap. A live raise to 360 writes 200.0 into the same slot, so the model is no longer reading the number it was trained on.
changing-fitted-cap.py
1def heartbeat_age_hours(raw_hours, cap_hours): 2 return min(raw_hours, cap_hours) 3 4raw_age = 200.0 5training_vector = heartbeat_age_hours(raw_age, 180.0) 6live_vector = heartbeat_age_hours(raw_age, 360.0) 7print("raw hours:", raw_age) 8print("training hb_age_h:", training_vector) 9print("live hb_age_h after cap change:", live_vector) 10print("same slot, different meaning:", training_vector != live_vector)
Output
1raw hours: 200.0 2training hb_age_h: 180.0 3live hb_age_h after cap change: 200.0 4same slot, different meaning: True

pandas and scikit-learn still have to ship that same cap. OrdinalEncoder can own the runner-pool codes only if other is a fitted category, not something you invent at serve time.

library-feature-contract.py
1import pandas as pd 2from sklearn.preprocessing import OrdinalEncoder 3 4rows = pd.DataFrame( 5 [ 6 {"runner_pool": "a100-pool", "queued_minutes": 42.0, "raw_heartbeat_hours": 0.5}, 7 {"runner_pool": "l4-pool", "queued_minutes": 18.0, "raw_heartbeat_hours": 200.0}, 8 ] 9) 10rows["hb_age_h"] = rows["raw_heartbeat_hours"].map(lambda hours: heartbeat_age_hours(hours, 180.0)) 11 12mapped_pools = rows["runner_pool"].map( 13 lambda pool: pool if pool in {"a100-pool", "h100-pool"} else "other" 14) 15encoder = OrdinalEncoder(categories=[["a100-pool", "h100-pool", "other"]]) 16rows["runner_pool_code"] = encoder.fit_transform(mapped_pools.to_frame()) 17 18print(rows[["runner_pool", "runner_pool_code", "hb_age_h"]].to_string(index=False)) 19print("unknown pools share the fitted other code:", set(rows.loc[rows["runner_pool"].eq("l4-pool"), "runner_pool_code"]) == {2.0})
Output
1runner_pool runner_pool_code hb_age_h 2 a100-pool 0.0 0.5 3 l4-pool 2.0 180.0 4unknown pools share the fitted other code: True

The pandas table didn't invent a new meaning. l4-pool still becomes fitted other (2.0), and the 200-hour heartbeat still clips to 180.0. Ship the encoder categories and the cap with the weights. Don't regenerate them from today's runner catalog.

A serving change raises the heartbeat-age cap from 180 hours to 360 hours and keeps the same model file. What broke?

Answer

The trained weights interpret heartbeat age under the fitted 180-hour cap. A 200-hour heartbeat that used to arrive as 180.0 now arrives as 200.0, so the same slot has a new meaning.

Compare the offline row with the live row

An offline notebook might compute queue backlog by scanning a completed daily table. The service might read an hourly cache. Even when both columns are named queue_backlog, differences in freshness or aggregation can change predictions. That failure is training-serving skew.

A feature store is one way to ship the same contract to both paths: historical rows for training, low-latency lookups for scoring. Feast's historical retrieval does a point-in-time join: it scans backward from each entity timestamp up to the feature view's TTL, then joins the latest eligible feature row.[2]Reference 2Point-in-time Joinshttps://docs.feast.dev/getting-started/concepts/point-in-time-joins Its online store keeps only the latest values per entity key, not the historical series.[3]Reference 3Online Storehttps://docs.feast.dev/getting-started/components/online-store A model release must name the feature definition and snapshot that produced its score. The store doesn't invent the cutoff, missing policy, or vector order. It executes them.

Sample offline and online paths on the same entities before promoting a model. Compare both a matching row and a stale-cache failure.

check-parity.py
1def mismatches(offline, online): 2 keys = offline.keys() | online.keys() 3 return {key: (offline.get(key), online.get(key)) for key in keys if offline.get(key) != online.get(key)} 4 5offline = {"runner_pool": "a100-pool", "queue_backlog": 18.0, "priority_job": 0} 6online_rows = { 7 "matching": {"runner_pool": "a100-pool", "queue_backlog": 18.0, "priority_job": 0}, 8 "stale cache": {"runner_pool": "a100-pool", "queue_backlog": 0.0, "priority_job": 0}, 9} 10 11for name, online in online_rows.items(): 12 differences = mismatches(offline, online) 13 print(name, "mismatches:", differences, "release allowed:", not differences)
Output
1matching mismatches: {} release allowed: True 2stale cache mismatches: {'queue_backlog': (18.0, 0.0)} release allowed: False

Matching names and types aren't enough. The 18.0 vs 0.0 backlog pair is a release blocker even though both dictionaries look well-formed.

Watch the contract before watching accuracy:

Production checkFailure it catchesAction
null rate by featureupstream feed disappearedfail closed or fallback
unseen-category raterunner-pool catalog changedcollect labels and retrain
freshness lagonline values are stalepause promotions
offline/online parity sampletransformations disagreerepair feature path

Parity strategy also depends on where the feature is computed:

Feature kindProduction pathParity strategy
request-timecomputed on the serving path from the live request and request-local contextlog raw request fields and recompute the training transform offline
precomputed storewritten to an online feature store ahead of the requestdual-write or dual-read the store path, plus point-in-time replay for training rows

Request-path fields need log-and-recompute. Precomputed store fields need dual-write (or dual-read) plus PIT replay. Both still need a shared transformation contract. Only the evidence path differs.

Freshness failures need an explicit product policy. This one tries normal scoring, fallback, then abstention. The 15 / 45 minute cutoffs are a local product policy, not a universal constant. Other lessons may choose different numbers for the same idea. Version the thresholds with the serving path.

apply-freshness-policy.py
1def scoring_policy(freshness_lag_minutes): 2 if freshness_lag_minutes <= 15: 3 return "normal scoring" 4 if freshness_lag_minutes <= 45: 5 return "fallback model" 6 return "abstain and alert" 7 8for lag in [8, 30, 90]: 9 print(f"{lag} minutes -> {scoring_policy(lag)}")
Output
18 minutes -> normal scoring 230 minutes -> fallback model 390 minutes -> abstain and alert

The 09:00 row is now a contract: cutoff, types, missing rules, fitted maps, vector order, and a parity sample. It still assumes every heartbeat is visible as soon as it occurs. Real pipelines have a second clock.

Offline and online transforms return different queue_backlog values for the same entity and cutoff. What should the release check do?

Answer

Block release and inspect freshness, aggregation, and feature-definition versions. Matching column names don't establish training-serving parity.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A source record contains finished_at for later label construction. During feature construction, finished_at appears in the emitted model-input dictionary even though the output allowlist contains only runner_pool, queued_minutes, hours_since_last_heartbeat, queue_backlog, queue_backlog_missing, and priority_job. Which gate response is correct?

Correct answer: Reject unexpected finished_at; still apply cutoff filtering to every allowed value.

A job-SLA outcome can be joined later to create the label, but it can't enter the model input for an earlier prediction. An output allowlist fails when an unexpected key appears, but it only checks keys. Timestamp filtering still has to confirm allowed fields were computed from visible history.

2.During a scheduler telemetry feed outage, queue_backlog is unavailable. A feature job writes queue_backlog = 0.0 and doesn't emit queue_backlog_missing. What is the main contract problem?

Correct answer: The model can't distinguish an unavailable measurement from a measured empty queue, so an outage can look like no congestion.

A measured queue backlog of zero means the queue was observed to be empty. A missing queue backlog means the measurement failed or was unavailable. The contract preserves that difference with queue_backlog_missing or abstains instead of silently treating unknown congestion as no congestion.

3.Serving will load trained weights for job-SLA prediction. Engineers also want to rebuild the runner-pool mapping from today's runner-pool catalog and change the heartbeat-age cap from 180 to 360 hours while keeping feature names unchanged. What package avoids changing the input meaning?

Correct answer: The fitted feature contract, runner_pool mapping with other, heartbeat-age cap, fixed vector order, training snapshot, and parity evidence.

The trained weights interpret each vector position according to the transformations used during training. Regenerating the runner_pool mapping or changing the heartbeat-age cap can change the meaning of the same feature names, so fitted contracts and parity evidence must travel with the model.

4.At 2026-05-01T09:00:00, heartbeats are at 2026-05-01T01:00:00, 2026-05-01T08:30:00, and 2026-05-01T12:00:00. Which feature-construction result respects the prediction timestamp?

Correct answer: Two heartbeats are visible, the latest visible heartbeat is 08:30, and the 12:00 heartbeat is excluded.

Feature history must end at the prediction timestamp. Events at or before 09:00 are visible, while the 12:00 heartbeat is future information for that prediction and must be excluded rather than converted into a negative age.

5.An encoder was fitted with known_runner_pools = {'a100-pool', 'h100-pool'} and a reserved other bucket. How should it handle runner_pool='l4-pool' and runner_pool=None?

Correct answer: l4-pool maps to other; None is rejected as missing runner_pool data.

A well-formed unseen category can use the reserved other representation until retraining evaluates it. A missing runner_pool is different: it may indicate a broken source feed, so silently folding it into other would hide a data-quality failure.

6.For the same entity and cutoff, the offline path produces {'runner_pool': 'a100-pool', 'queue_backlog': 18.0, 'priority_job': 0} and the online path produces {'runner_pool': 'a100-pool', 'queue_backlog': 0.0, 'priority_job': 0}. What should the release gate do?

Correct answer: Block release and investigate training-serving skew in queue_backlog before promotion.

A parity sample compares the actual values produced by offline and online feature paths for the same entity and cutoff. The different queue_backlog values show that the paths disagree, so the model shouldn't be promoted until the feature path is repaired.

7.An online feature cache has freshness lag of 90 minutes. The serving path's versioned policy is: up to 15 minutes use normal scoring, up to 45 minutes use a fallback model, and above 45 minutes abstain and alert. What should happen?

Correct answer: Abstain and alert because the lag is outside the fallback window.

Freshness is part of the production feature contract. Under the stated policy, 90 minutes is beyond both the normal-scoring and fallback ranges, so the service should stop using the stale path and alert rather than silently score with stale values.

8.Assume the required timestamps and runner_pool are valid and present. Which input combination satisfies the row contract?

Correct answer: queued_minutes=42, priority_job=False, and measured queue_backlog=0.

The contract requires a finite nonnegative queued minutes, a true Boolean for priority_job, and a finite nonnegative queue backlog. A measured queue backlog of zero is valid, and so is queued_minutes=0. A negative queue time, a string that merely spells false, and a negative queue backlog must each cause rejection.

8 questions remaining.

Next Step
Continue to Batch and Streaming Feature Pipelines

You can now freeze one prediction-time row and reject future fields. Next you'll add a second clock, ingestion time, so a heartbeat that occurred before cutoff but arrived late can't leak into training or serving.

PreviousDataset Pipelines and Data Quality
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Hidden Technical Debt in Machine Learning Systems.

Sculley et al. · 2015

https://research.google/pubs/hidden-technical-debt-in-machine-learning-systems/

Point-in-time Joins

Feast Contributors · 2026 · Official documentation

https://docs.feast.dev/getting-started/concepts/point-in-time-joins

Online Store

Feast Contributors · 2026 · Official documentation

https://docs.feast.dev/getting-started/components/online-store

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.