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 SystemsBatch and Streaming Feature Pipelines
⚙️MediumMLOps & Deployment

Batch and Streaming Feature Pipelines

Replay job-SLA features at decision time from events, then keep that same meaning in online serving.

14 min read
Learning path
Step 43 of 178 in the full curriculum
Feature Engineering for Production MLGradient Boosted Trees in Production

Personalize this lesson

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

Job J-204 already has a cutoff. At 09:00 the model may use heartbeats from 01:00 and 08:30, and it must ignore the noon heartbeat. That filter treated arrival as instantaneous, so hours_since_last_heartbeat came out to 0.5.

Event buses don't deliver on that schedule. The 08:30 heartbeat can land at 11:00. Live scoring at 09:00 never saw it. If training still uses 0.5 hours, the fit is cheating on a number serving couldn't have computed.

One event has two relevant times

Take that late 08:30 heartbeat:

FieldExampleMeaning
event_timeMay 1, 08:30when the scheduler recorded the heartbeat
ingested_atMay 1, 11:00when this pipeline learned about the heartbeat
prediction_timeMay 1, 09:00when scoring asked for J-204's feature row

The heartbeat happened before the scoring request, but it arrived two hours afterward. A live prediction at 09:00 couldn't use it. A faithful replay excludes facts that occurred too late or arrived too late:

tevent≤tpandtingested≤tpt_{\mathrm{event}} \le t_p \quad\text{and}\quad t_{\mathrm{ingested}} \le t_ptevent​≤tp​andtingested​≤tp​

teventt_{\mathrm{event}}tevent​ is when the source says the heartbeat happened. tingestedt_{\mathrm{ingested}}tingested​ is when this pipeline first observed it. tpt_ptp​ is the prediction timestamp. The first inequality blocks future-event leakage. The second reproduces what the live system knew. Keep both timestamps even if your first source rarely arrives late.

Two-clock plot for job J-204 at 09:00: H1 at event time 01:00 and ingest 01:02 sits in the known quadrant, H2 at 08:30 ingested at 11:00 is a late arrival, and H3 at 12:00 is a future event. The side panel shows as-of heartbeat age advancing from 8.0 hours at 09:00 to 3.5 hours at 12:00 to 4.0 hours at 16:00.
At 09:00, only H1 lies inside both time boundaries, so heartbeat age is 8.0h. H2 is the subtle failure: it occurred at 08:30 but arrived at 11:00, so an event-time-only replay would leak the previous chapter's 0.5h number.

A heartbeat occurred at 08:30 but wasn't ingested until 11:00. Can a prediction made at 09:00 use it?

Answer

No. A faithful replay requires both event_time <= prediction_time and ingested_at <= prediction_time. The heartbeat had happened, but the live system didn't know about it yet.

Rebuild what serving knew

Build the replay in small steps. The first cell defines one on-time heartbeat, one late arrival, and one future heartbeat for job J-204. Run the cells in order.

01-inspect-event-clocks.py
1from datetime import datetime 2from datetime import timedelta 3 4def dt(value): 5 return datetime.fromisoformat(value) 6 7events = [ 8 { 9 "id": "H1", 10 "job": "J-204", 11 "event_time": dt("2026-05-01T01:00:00"), 12 "ingested_at": dt("2026-05-01T01:02:00"), 13 "source_version": 1, 14 }, 15 { 16 "id": "H2", 17 "job": "J-204", 18 "event_time": dt("2026-05-01T08:30:00"), 19 "ingested_at": dt("2026-05-01T11:00:00"), 20 "source_version": 1, 21 }, 22 { 23 "id": "H3", 24 "job": "J-204", 25 "event_time": dt("2026-05-01T12:00:00"), 26 "ingested_at": dt("2026-05-01T12:01:00"), 27 "source_version": 1, 28 }, 29] 30prediction_time = dt("2026-05-01T09:00:00") 31 32for event in events: 33 lag_minutes = int((event["ingested_at"] - event["event_time"]).total_seconds() / 60) 34 print( 35 f'{event["id"]} occurred={event["event_time"]:%H:%M} ' 36 f'arrived={event["ingested_at"]:%H:%M} lag={lag_minutes}m' 37 )
Output
1H1 occurred=01:00 arrived=01:02 lag=2m 2H2 occurred=08:30 arrived=11:00 lag=150m 3H3 occurred=12:00 arrived=12:01 lag=1m

A filter on event_time alone looks reasonable, but it leaks H2 into a replay of the 09:00 decision and recreates the dishonest 0.5h age.

02-expose-event-time-only-bug.py
1def event_time_only(events, job_id, at): 2 return [ 3 event 4 for event in events 5 if event["job"] == job_id and event["event_time"] <= at 6 ] 7 8def heartbeat_age_hours(visible, at): 9 latest = max(visible, key=lambda event: event["event_time"]) 10 return (at - latest["event_time"]).total_seconds() / 3600 11 12naive_replay = event_time_only(events, "J-204", prediction_time) 13print("event-time-only ids:", [event["id"] for event in naive_replay]) 14print(f"event-time-only age: {heartbeat_age_hours(naive_replay, prediction_time):.1f}h") 15print("leaked late heartbeat:", any(event["id"] == "H2" for event in naive_replay))
Output
1event-time-only ids: ['H1', 'H2'] 2event-time-only age: 0.5h 3leaked late heartbeat: True

Add the availability boundary. Now replay sees the same facts live serving saw at 09:00, and age jumps back to 8.0h.

03-filter-by-decision-time.py
1def known_by(events, job_id, at): 2 return [ 3 event 4 for event in events 5 if ( 6 event["job"] == job_id 7 and event["event_time"] <= at 8 and event["ingested_at"] <= at 9 ) 10 ] 11 12faithful_replay = known_by(events, "J-204", prediction_time) 13print("decision-time ids:", [event["id"] for event in faithful_replay]) 14print(f"decision-time age: {heartbeat_age_hours(faithful_replay, prediction_time):.1f}h") 15print("late heartbeat excluded:", all(event["id"] != "H2" for event in faithful_replay))
Output
1decision-time ids: ['H1'] 2decision-time age: 8.0h 3late heartbeat excluded: True

A historical training snapshot needs the latest visible heartbeat for each scoring request. H2 becomes usable after it arrives at 11:00; H3 becomes usable after 12:01. Age is computed at the cutoff from that latest timestamp, not cached from the last stream write.

04-build-as-of-rows.py
1def state_order(event): 2 return (event["event_time"], event.get("source_version", 0)) 3 4def revision_value(event): 5 return event.get("phase", event["event_time"]) 6 7def as_of_hours(events, job_id, at): 8 visible = known_by(events, job_id, at) 9 if not visible: 10 return "missing" 11 latest_order = max(state_order(event) for event in visible) 12 latest = [event for event in visible if state_order(event) == latest_order] 13 if len({revision_value(event) for event in latest}) > 1: 14 return "quarantined" 15 hours = (at - latest[0]["event_time"]).total_seconds() / 3600 16 return hours 17 18for requested_at in [ 19 dt("2026-05-01T09:00:00"), 20 dt("2026-05-01T12:00:00"), 21 dt("2026-05-01T16:00:00"), 22]: 23 age = as_of_hours(events, "J-204", requested_at) 24 rendered = f"{age:.1f}h" if isinstance(age, float) else age 25 print(f"{requested_at:%H:%M} -> {rendered}")
Output
109:00 -> 8.0h 212:00 -> 3.5h 316:00 -> 4.0h

Analysts may also need corrected history of what happened by 09:00, regardless of arrival time. Keep that artifact separate: corrected history may include H2, while the decision-time replay contains only H1.

Each label has its own cutoff

Two-clock filtering still isn't enough if you build every training row from one global table snapshot. Table time travel in a Delta Lake or Apache Iceberg table restores the whole table as of one snapshot or transaction time, such as 2026-05-01 09:30:00. Every row comes from that same clock.

In ML training, each label row is a separate prediction with its own tpt_ptp​. Job J-201 might be scored at 09:00 while J-202 is scored at 09:30. A point-in-time join searches backward from each row's timestamp for the latest feature state visible at that moment.

If you query a single snapshot at 09:30 to construct queue_backlog for both jobs:

  • J-202 (predicted at 09:30) gets the correct state.
  • J-201 (predicted at 09:00) picks up the 09:15 backlog update. That's leakage.

The next snippet implements both joins in ordinary Python. Each label keeps its own cutoff; the leaky path reuses one snapshot time for every job.

pit-vs-snapshot-join.py
1from datetime import datetime 2 3def ts(value): 4 return datetime.fromisoformat(value) 5 6labels = [ 7 {"job": "J-201", "prediction_time": ts("2026-05-01T09:00:00")}, 8 {"job": "J-202", "prediction_time": ts("2026-05-01T09:30:00")}, 9] 10feature_updates = [ 11 {"job": "J-201", "time": ts("2026-05-01T08:50:00"), "queue_backlog": 5}, 12 {"job": "J-201", "time": ts("2026-05-01T09:15:00"), "queue_backlog": 12}, 13 {"job": "J-202", "time": ts("2026-05-01T09:20:00"), "queue_backlog": 8}, 14] 15 16def asof_join(labels, updates): 17 rows = [] 18 for label in labels: 19 visible = [ 20 update 21 for update in updates 22 if update["job"] == label["job"] and update["time"] <= label["prediction_time"] 23 ] 24 latest = max(visible, key=lambda update: update["time"]) if visible else None 25 rows.append({ 26 "job": label["job"], 27 "prediction_time": label["prediction_time"], 28 "queue_backlog": None if latest is None else latest["queue_backlog"], 29 }) 30 return rows 31 32def snapshot_join(labels, updates, snapshot_time): 33 latest_by_job = {} 34 for update in updates: 35 if update["time"] <= snapshot_time: 36 previous = latest_by_job.get(update["job"]) 37 if previous is None or update["time"] > previous["time"]: 38 latest_by_job[update["job"]] = update 39 return [ 40 { 41 "job": label["job"], 42 "prediction_time": label["prediction_time"], 43 "queue_backlog": latest_by_job[label["job"]]["queue_backlog"], 44 } 45 for label in labels 46 ] 47 48pit_join = asof_join(labels, feature_updates) 49leakage_join = snapshot_join(labels, feature_updates, ts("2026-05-01T09:30:00")) 50 51print("ASOF (PIT) JOIN:") 52for row in pit_join: 53 print(f'{row["job"]} {row["prediction_time"]:%H:%M} backlog={row["queue_backlog"]}') 54print("SNAPSHOT JOIN (LEAKAGE):") 55for row in leakage_join: 56 print(f'{row["job"]} {row["prediction_time"]:%H:%M} backlog={row["queue_backlog"]}') 57 58assert next(row["queue_backlog"] for row in pit_join if row["job"] == "J-201") == 5 59assert next(row["queue_backlog"] for row in leakage_join if row["job"] == "J-201") == 12
Output
1ASOF (PIT) JOIN: 2J-201 09:00 backlog=5 3J-202 09:30 backlog=8 4SNAPSHOT JOIN (LEAKAGE): 5J-201 09:00 backlog=12 6J-202 09:30 backlog=8

The same per-row rule in pandas is merge_asof. Sort both frames by the join clock, then look backward within each job. It has to return J-201 = 5, not the leaky snapshot 12.

pandas-merge-asof-pit.py
1from datetime import datetime 2 3import pandas as pd 4 5def ts(value): 6 return datetime.fromisoformat(value) 7 8labels = pd.DataFrame( 9 [ 10 {"job": "J-201", "prediction_time": ts("2026-05-01T09:00:00")}, 11 {"job": "J-202", "prediction_time": ts("2026-05-01T09:30:00")}, 12 ] 13).sort_values("prediction_time") 14updates = pd.DataFrame( 15 [ 16 {"job": "J-201", "prediction_time": ts("2026-05-01T08:50:00"), "queue_backlog": 5}, 17 {"job": "J-201", "prediction_time": ts("2026-05-01T09:15:00"), "queue_backlog": 12}, 18 {"job": "J-202", "prediction_time": ts("2026-05-01T09:20:00"), "queue_backlog": 8}, 19 ] 20).sort_values("prediction_time") 21 22pit = pd.merge_asof( 23 labels, 24 updates, 25 by="job", 26 on="prediction_time", 27 direction="backward", 28) 29 30for row in pit.itertuples(index=False): 31 print(f"{row.job} {row.prediction_time:%H:%M} backlog={row.queue_backlog}") 32assert int(pit.loc[pit["job"].eq("J-201"), "queue_backlog"].iloc[0]) == 5
Output
1J-201 09:00 backlog=5 2J-202 09:30 backlog=8

Feature stores commonly wrap that backward search as historical retrieval. Feast scans backward from each entity timestamp up to that row's own time-to-live (TTL) window, not backward from "now."[1]Reference 1Point-in-time Joinshttps://docs.feast.dev/getting-started/concepts/point-in-time-joins Your pipeline still owns the exact replay contract, including whether ingestion time belongs in it. The pandas call only implements the join clock you already chose.

Keep online state current

Batch replay, stream updates, and online reads are related but distinct jobs:

JobReadsProducesTypical latency
batch replaybounded event historyversioned training snapshotminutes or hours
stream updaternewly arrived eventscurrent feature stateseconds
online readcurrent feature stateone scoring rowmilliseconds
Diagram showing Event log event_time + ingested_at, Feature definition v1 time boundary + missing policy, Batch replay historical training rows, and Stream updater newly arrived events.
Event log event_time + ingested_at, Feature definition v1 time boundary + missing policy, Batch replay historical training rows, and Stream updater newly arrived events.

Different stores are acceptable. Different meanings aren't. If batch computes seven-day mean backlog while online returns a one-hour queue count under the same name, offline metrics can't predict live behavior. That gap is training-serving skew.

Feast's online store is built for low-latency lookups: for each entity key it keeps only the latest feature values, not full history.[2]Reference 2Online Storehttps://docs.feast.dev/getting-started/components/online-store Google Cloud's MLOps guidance describes a feature store as an optional shared repository for definitions, storage, and access across high-throughput batch and low-latency serving workloads.[3]Reference 3MLOps: Continuous Delivery and Automation Pipelines in Machine Learning.https://docs.cloud.google.com/architecture/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning

Store timestamps, not precomputed ages. hours_since_last_heartbeat depends on the scoring request's clock. If the stream updater writes 0.03 at 01:02 and no later heartbeat arrives, a 09:00 read must still report 8.0, not 0.03.

A late, older event mustn't overwrite newer online state. Same-time corrections need a deterministic rule too. Use the same source-owned (event_time, source_version) order in batch replay and online updates: version 2 replaces version 1, including when a stale version-1 retry arrives later. In a real source contract, require a stable sequence or revision field rather than trusting arrival order.

06-protect-latest-online-state.py
1online_state = {} 2 3def apply_latest_state(state, event): 4 previous = state.get(event["job"]) 5 if previous is not None: 6 if event["id"] == previous.get("id"): 7 return "ignored duplicate event" 8 if state_order(event) < state_order(previous): 9 return "ignored stale event" 10 if state_order(event) == state_order(previous): 11 if previous.get("quarantined"): 12 return "quarantine persists" 13 if revision_value(event) == revision_value(previous): 14 return "ignored duplicate revision" 15 state[event["job"]] = { 16 "job": event["job"], 17 "event_time": event["event_time"], 18 "source_version": event.get("source_version", 0), 19 "phase": "quarantined", 20 "quarantined": True, 21 } 22 return "quarantined conflicting revision" 23 state[event["job"]] = event 24 return "stored" 25 26def online_age_hours(state, job_id, requested_at): 27 current = state.get(job_id) 28 if current is None: 29 return "missing" 30 if current.get("quarantined"): 31 return "quarantined" 32 return (requested_at - current["event_time"]).total_seconds() / 3600 33 34arrivals = [ 35 { 36 "id": "H4", 37 "job": "J-204", 38 "event_time": dt("2026-05-01T10:30:00"), 39 "ingested_at": dt("2026-05-01T10:31:00"), 40 "source_version": 1, 41 "phase": "worker_scaled", 42 }, 43 { 44 "id": "H4-correction", 45 "job": "J-204", 46 "event_time": dt("2026-05-01T10:30:00"), 47 "ingested_at": dt("2026-05-01T10:34:00"), 48 "source_version": 2, 49 "phase": "worker_scaled_corrected", 50 }, 51 { 52 "id": "H4-v1-retry", 53 "job": "J-204", 54 "event_time": dt("2026-05-01T10:30:00"), 55 "ingested_at": dt("2026-05-01T10:36:00"), 56 "source_version": 1, 57 "phase": "worker_scaled", 58 }, 59 { 60 "id": "H1-retry", 61 "job": "J-204", 62 "event_time": dt("2026-05-01T01:00:00"), 63 "ingested_at": dt("2026-05-01T11:05:00"), 64 "source_version": 1, 65 }, 66] 67 68for event in arrivals: 69 print(f'{event["id"]}: {apply_latest_state(online_state, event)}') 70age_at_1040 = online_age_hours(online_state, "J-204", dt("2026-05-01T10:40:00")) 71print(f"age at 10:40: {age_at_1040:.2f}h") 72print("stored version:", online_state["J-204"]["source_version"])
Output
1H4: stored 2H4-correction: stored 3H4-v1-retry: ignored stale event 4H1-retry: ignored stale event 5age at 10:40: 0.17h 6stored version: 2

Latest-value state isn't enough for aggregates. A duplicated queue update can silently inflate queue_backlog unless the updater is idempotent. Idempotent means retrying the same event doesn't change the result after its first successful application. The updater should also reject impossible aggregate states instead of publishing them.

07-deduplicate-aggregate-updates.py
1backlog_state = {"seen": set(), "origin_backlog": 0} 2 3def apply_queue_update(state, event): 4 if event["id"] in state["seen"]: 5 return "duplicate skipped" 6 next_backlog = state["origin_backlog"] + event["delta"] 7 if next_backlog < 0: 8 return "blocked negative backlog" 9 state["seen"].add(event["id"]) 10 state["origin_backlog"] = next_backlog 11 return "applied" 12 13queue_updates = [ 14 {"id": "Q1", "delta": 3}, 15 {"id": "Q1", "delta": 3}, 16 {"id": "Q2", "delta": -1}, 17 {"id": "Q3", "delta": -5}, 18] 19 20for update in queue_updates: 21 result = apply_queue_update(backlog_state, update) 22 print(f'{update["id"]}: {result}; backlog={backlog_state["origin_backlog"]}')
Output
1Q1: applied; backlog=3 2Q1: duplicate skipped; backlog=3 3Q2: applied; backlog=2 4Q3: blocked negative backlog; backlog=2

The set makes sequential retries visible, but a live updater needs a durable atomic write: record the event ID and apply its aggregate change together. Otherwise a worker can crash between those steps and a retry can lose or repeat an update. Use a transaction, conditional write, or stream processor state primitive with the same contract.

A version-2 correction and a later retry of version 1 share the same event time. Which value should remain online?

Answer

Version 2. Ordering by (event_time, source_version) prevents a late retry from moving current state backward. If both ordering fields match but values conflict, quarantine the state instead of choosing by arrival order.

Bound lateness and freshness

A stream processor can't wait forever for missing events. It tracks a watermark: a declaration that event time has reached time ttt, so events with timestamps at or before ttt should already have arrived. That declaration isn't a guarantee. An older timestamp can still show up later. A product policy then decides whether the late event may update a recent window or must wait for a controlled replay.

This miniature policy accepts updates no more than 45 minutes behind the watermark. An event whose timestamp is still ahead of the watermark stays on the on-time path. A real stream processor also defines windows, triggers, state cleanup, and monitoring.

08-classify-late-events.py
1def late_event_action(event_time, watermark, allowed_lateness): 2 if event_time > watermark: 3 return "on-time path" 4 lateness = watermark - event_time 5 if lateness <= allowed_lateness: 6 return "accept late update" 7 return "quarantine for replay" 8 9watermark = dt("2026-05-01T12:00:00") 10allowed_lateness = timedelta(minutes=45) 11 12for event_time in [ 13 dt("2026-05-01T12:05:00"), 14 dt("2026-05-01T11:30:00"), 15 dt("2026-05-01T10:00:00"), 16]: 17 print(f"{event_time:%H:%M} -> {late_event_action(event_time, watermark, allowed_lateness)}")
Output
112:05 -> on-time path 211:30 -> accept late update 310:00 -> quarantine for replay

Freshness is a serving policy on top of that stream. The feature contract from the previous chapter already used a 15-minute normal window and a 45-minute fallback. Encode it against the stored timestamp, not against a cached age.

09-apply-serving-freshness-policy.py
1def serving_mode(updated_at, requested_at): 2 lag_minutes = int((requested_at - updated_at).total_seconds() / 60) 3 if lag_minutes < 0: 4 raise ValueError("feature update is from the future") 5 if lag_minutes <= 15: 6 return "normal scoring" 7 if lag_minutes <= 45: 8 return "fallback and log degraded mode" 9 return "abstain from narrow SLA estimate" 10 11requested_at = dt("2026-05-01T12:00:00") 12for updated_at in [ 13 dt("2026-05-01T11:52:00"), 14 dt("2026-05-01T11:30:00"), 15 dt("2026-05-01T10:30:00"), 16]: 17 print(f"{updated_at:%H:%M} -> {serving_mode(updated_at, requested_at)}")
Output
111:52 -> normal scoring 211:30 -> fallback and log degraded mode 310:30 -> abstain from narrow SLA estimate

No model fixes a missing or temporally invalid input. Record freshness lag with each prediction trace so operators can distinguish model error from stale state.

Compare offline and online rows

Before promotion, replay a sample of requests offline and compare those rows with rows captured from online serving. Start with exact equality for categorical and integer fields; define tolerances explicitly if floating-point aggregations need them. For heartbeat age, both paths must compute hours at the same tpt_ptp​ from the same stored timestamp.

10-check-offline-online-parity.py
1def mismatches(left, right): 2 keys = left.keys() | right.keys() 3 return {key: (left.get(key), right.get(key)) for key in keys if left.get(key) != right.get(key)} 4 5correction_offline_row = { 6 "hours_since_last_heartbeat": as_of_hours(arrivals, "J-204", dt("2026-05-01T10:40:00")), 7} 8correction_online_row = { 9 "hours_since_last_heartbeat": online_age_hours( 10 online_state, "J-204", dt("2026-05-01T10:40:00") 11 ), 12} 13correction_differences = mismatches(correction_offline_row, correction_online_row) 14print("same-time retry mismatches:", correction_differences) 15 16conflicting_revisions = [ 17 { 18 "id": "H5-a", 19 "job": "J-205", 20 "event_time": dt("2026-05-01T10:30:00"), 21 "ingested_at": dt("2026-05-01T10:31:00"), 22 "source_version": 7, 23 "phase": "worker_scaled", 24 }, 25 { 26 "id": "H5-b", 27 "job": "J-205", 28 "event_time": dt("2026-05-01T10:30:00"), 29 "ingested_at": dt("2026-05-01T10:32:00"), 30 "source_version": 7, 31 "phase": "worker_scale_failed", 32 }, 33] 34conflict_online_state = {} 35for event in conflicting_revisions: 36 apply_latest_state(conflict_online_state, event) 37conflict_offline = as_of_hours(conflicting_revisions, "J-205", dt("2026-05-01T10:40:00")) 38conflict_online = online_age_hours( 39 conflict_online_state, "J-205", dt("2026-05-01T10:40:00") 40) 41assert conflict_offline == conflict_online == "quarantined" 42print("equal-order conflict parity:", conflict_offline, "==", conflict_online) 43 44for event in events: 45 if event["ingested_at"] <= dt("2026-05-01T16:00:00"): 46 apply_latest_state(online_state, event) 47 48offline_row = { 49 "hours_since_last_heartbeat": as_of_hours(events, "J-204", dt("2026-05-01T16:00:00")), 50 "origin_backlog": backlog_state["origin_backlog"], 51} 52online_row = { 53 "hours_since_last_heartbeat": online_age_hours( 54 online_state, "J-204", dt("2026-05-01T16:00:00") 55 ), 56 "origin_backlog": backlog_state["origin_backlog"], 57} 58 59print("offline row:", offline_row) 60print("online row:", online_row) 61print("mismatches:", mismatches(offline_row, online_row))
Output
1same-time retry mismatches: {} 2equal-order conflict parity: quarantined == quarantined 3offline row: {'hours_since_last_heartbeat': 4.0, 'origin_backlog': 2} 4online row: {'hours_since_last_heartbeat': 4.0, 'origin_backlog': 2} 5mismatches: {}

A stale online row produces a visible failure instead of a mysterious model regression.

11-detect-stale-online-state.py
1stale_online_row = { 2 "hours_since_last_heartbeat": 0.03, 3 "origin_backlog": backlog_state["origin_backlog"], 4} 5differences = mismatches(offline_row, stale_online_row) 6print("mismatches:", differences) 7print("release allowed:", not differences)
Output
1mismatches: {'hours_since_last_heartbeat': (4.0, 0.03)} 2release allowed: False

Publish a compact receipt beside every snapshot and candidate model. It makes replay semantics inspectable instead of leaving them hidden in job code.

12-publish-replay-receipt.py
1receipt = { 2 "feature_definition": "job_sla_features_v1", 3 "replay_boundary": "event_time <= prediction_time and ingested_at <= prediction_time", 4 "offline_online_ordering": "(event_time, source_version); equal-key conflicts quarantine", 5 "age_rule": "store last heartbeat timestamp; compute hours at request time", 6 "aggregate_updates": "atomic event-id dedup; reject negative backlog", 7 "snapshot": "job-sla-train-2026-05-01", 8 "parity_samples_checked": 2, 9 "freshness_policy": "sla-freshness-v1", 10} 11 12assert as_of_hours(events, "J-204", dt("2026-05-01T09:00:00")) == 8.0 13assert as_of_hours(events, "J-204", dt("2026-05-01T12:00:00")) == 3.5 14assert not correction_differences 15assert not mismatches(offline_row, online_row) 16 17for key, value in receipt.items(): 18 print(f"{key}={value}")
Output
1feature_definition=job_sla_features_v1 2replay_boundary=event_time <= prediction_time and ingested_at <= prediction_time 3offline_online_ordering=(event_time, source_version); equal-key conflicts quarantine 4age_rule=store last heartbeat timestamp; compute hours at request time 5aggregate_updates=atomic event-id dedup; reject negative backlog 6snapshot=job-sla-train-2026-05-01 7parity_samples_checked=2 8freshness_policy=sla-freshness-v1

Offline replay says heartbeat age 4.0, while the captured online row says 0.03 for the same job and cutoff. Should the model release proceed?

Answer

No. The mismatch shows that replay and serving disagree on definition, ordering, or freshness. Fix that parity failure before using offline model quality as evidence for live behavior.

When the contract breaks

SymptomCauseFix
Training gets better after a backfill but serving doesn'treplay used facts that arrived laterfilter by event time and ingestion time
Online heartbeat age stays near zero while the job goes silentage was cached at write timestore last_heartbeat_at and compute hours at the scoring request
Online status moves backward after retry or same-time corrections disagreestale or ambiguous event overwrote latest statecompare event timestamp plus source revision; quarantine unresolved ties
Backlog inflates during redeliveryaggregate update ran twicededuplicate by stable event ID in same atomic write as aggregate change
Reliable model emits bad SLA estimates during upstream lagfreshness wasn't part of serving policytrace age and fail to a safer response
Complete the lesson

Mastery Check

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

1.For job J-204, a prediction is requested at 09:00. H1 occurred at 01:00 and arrived at 01:02; H2 occurred at 08:30 and arrived at 11:00; H3 occurred at 12:00 and arrived at 12:01. What heartbeat age should a faithful replay expose at 09:00?

Correct answer: 8.0 hours, because only H1 both occurred and arrived before 09:00.

A decision-time replay must satisfy both event_time <= prediction_time and ingested_at <= prediction_time. H1 passes both checks, so age is 09:00 minus 01:00. H2 happened before 09:00 but wasn't available until 11:00, and H3 hadn't happened yet.

2.At 09:00, J-204 has H1 at 01:00 ingested at 01:02, and H2 at 08:30 ingested at 11:00. An analyst wants a corrected audit of what physically happened by 09:00, while the training row must replay what the scoring service knew at 09:00. Which handling is correct?

Correct answer: Keep separate artifacts: the audit view may include H1 and H2, but the training replay exposes only H1.

Corrected event-time history answers what truly happened by 09:00, so it can include a late-arriving 08:30 heartbeat. A training replay for the 09:00 model decision must reproduce the information available at 09:00, so it excludes that heartbeat until after it arrives.

3.J-201 is scored at 09:00 and J-202 is scored at 09:30. Queue backlog updates are J-201 at 08:50 (value 5), J-201 at 09:15 (value 12), and J-202 at 09:20 (value 8). A pipeline builds both training rows from one table snapshot taken at 09:30. What goes wrong?

Correct answer: J-201 gets backlog 12, leaking the 09:15 update that wasn't visible at 09:00.

A table snapshot uses one global clock. At 09:30 the latest J-201 update is the 09:15 row with backlog 12. A point-in-time join would have used J-201's own 09:00 cutoff and kept backlog 5.

4.A heartbeat arrives on the event stream at 10:31 and the job-SLA service asks for a feature row at 10:32. Which division of work matches the pipeline contract?

Correct answer: The stream updater consumes the heartbeat and writes current feature state; the online read retrieves that state for the scoring request.

The stream updater and the online read are separate jobs. The updater processes newly arrived events and maintains current feature state, while the online read serves one low-latency scoring row from that state.

5.The stream updater last wrote hours_since_last_heartbeat=0.03 when H1 arrived at 01:02. No later heartbeat has landed. A scoring request arrives at 09:00. What should the online path return?

Correct answer: 8.0 hours, computed at request time from the stored last_heartbeat_at of 01:00.

Heartbeat age depends on the scoring clock. The online store should keep the last visible heartbeat timestamp and compute hours at request time. Returning the 01:02 cached age of 0.03 hours would disagree with a faithful 09:00 replay.

6.A team keeps historical features in a batch store and latest values in a low-latency online store. What condition makes this acceptable before model promotion?

Correct answer: Both paths implement the same versioned feature meaning, and sampled offline rows match captured online rows within declared tolerances.

Different stores are fine because batch replay and online serving have different latency and retention needs. They aren't fine if they compute different feature meanings. Promotion should be gated by parity checks on sampled requests, with explicit tolerances where needed.

7.J-204 receives H4 at event_time 10:30 with source_version 1, H4-correction at the same time with source_version 2, then a delayed retry of H4 version 1. Batch replay and the online updater both order by (event_time, source_version). What should both expose at 10:40?

Correct answer: The version-2 heartbeat, because it wins in both paths and the same-time version-1 retry is stale.

Batch and online selection must implement the same feature meaning. Comparing (event_time, source_version) makes the version-2 correction newer than both version-1 copies even though one stale copy arrives last, so the sampled rows remain equal.

8.An aggregate updater for origin_backlog starts at 0 and may retry after worker crashes. It receives Q1 delta +3, duplicate Q1 delta +3, Q2 delta -1, and Q3 delta -5. Which implementation contract preserves the intended published state?

Correct answer: Atomically record each event id with its backlog change; count Q1 once, apply Q2, reject Q3, and publish backlog 2.

Idempotency requires a stable event id so a duplicate Q1 changes the backlog only once. The negative-state guard rejects Q3 because it would move the backlog from 2 to -3. The event-id record and the aggregate mutation must be one durable atomic operation; otherwise a crash between the two steps can lose or repeat an update.

9.With watermark 12:00 and allowed lateness 45 minutes, a stream processor sees events with event_time 12:05, 11:30, and 10:00. How should they be routed under this policy?

Correct answer: 12:05 uses on-time path; 11:30 is accepted as a late update; 10:00 is quarantined for replay.

An event after the watermark is still on the on-time path. An event 30 minutes behind a 12:00 watermark is within the 45-minute allowed-lateness window, so it can update. An event 120 minutes behind the watermark exceeds the policy and must wait for controlled replay.

10.A serving policy for job-SLA features is: lag <= 15 minutes uses normal scoring, lag <= 45 minutes falls back and logs degraded mode, and larger lag abstains from a narrow SLA estimate. At 12:00 the feature was last updated at 10:30. What should serving do?

Correct answer: Abstain from a narrow SLA estimate and record the 90-minute freshness lag with the prediction trace.

The feature is 90 minutes old at request time, which exceeds the 45-minute degraded-mode limit. A serving policy should fail to a safer response, and tracing the lag helps operators distinguish stale state from model error.

10 questions remaining.

Next Step
Continue to Gradient Boosted Trees in Production

Honest rows now carry heartbeat age and backlog as serving would have seen them. Next you'll fit a boosted tree on those columns and decide whether the validation evidence is enough to deploy.

PreviousFeature Engineering for Production ML
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

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

MLOps: Continuous Delivery and Automation Pipelines in Machine Learning.

Google Cloud. · 2026 · Official documentation

https://docs.cloud.google.com/architecture/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.