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:
| Field | Example | Meaning |
|---|---|---|
event_time | May 1, 08:30 | when the scheduler recorded the heartbeat |
ingested_at | May 1, 11:00 | when this pipeline learned about the heartbeat |
prediction_time | May 1, 09:00 | when 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:
is when the source says the heartbeat happened. is when this pipeline first observed it. 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.

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.
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 )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=1mA 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.
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))1event-time-only ids: ['H1', 'H2']
2event-time-only age: 0.5h
3leaked late heartbeat: TrueAdd the availability boundary. Now replay sees the same facts live serving saw at 09:00, and age jumps back to 8.0h.
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))1decision-time ids: ['H1']
2decision-time age: 8.0h
3late heartbeat excluded: TrueA 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.
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}")109:00 -> 8.0h
212:00 -> 3.5h
316:00 -> 4.0hAnalysts 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 . 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 at09:30) gets the correct state.J-201(predicted at09:00) picks up the09:15backlog 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.
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") == 121ASOF (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=8The 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.
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]) == 51J-201 09:00 backlog=5
2J-202 09:30 backlog=8Feature 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] 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:
| Job | Reads | Produces | Typical latency |
|---|---|---|---|
| batch replay | bounded event history | versioned training snapshot | minutes or hours |
| stream updater | newly arrived events | current feature state | seconds |
| online read | current feature state | one scoring row | milliseconds |

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] 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]
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.
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"])1H4: stored
2H4-correction: stored
3H4-v1-retry: ignored stale event
4H1-retry: ignored stale event
5age at 10:40: 0.17h
6stored version: 2Latest-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.
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"]}')1Q1: applied; backlog=3
2Q1: duplicate skipped; backlog=3
3Q2: applied; backlog=2
4Q3: blocked negative backlog; backlog=2The 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 , so events with timestamps at or before 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.
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)}")112:05 -> on-time path
211:30 -> accept late update
310:00 -> quarantine for replayFreshness 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.
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)}")111:52 -> normal scoring
211:30 -> fallback and log degraded mode
310:30 -> abstain from narrow SLA estimateNo 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 from the same stored timestamp.
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))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.
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)1mismatches: {'hours_since_last_heartbeat': (4.0, 0.03)}
2release allowed: FalsePublish a compact receipt beside every snapshot and candidate model. It makes replay semantics inspectable instead of leaving them hidden in job code.
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}")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-v1Offline 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
| Symptom | Cause | Fix |
|---|---|---|
| Training gets better after a backfill but serving doesn't | replay used facts that arrived later | filter by event time and ingestion time |
| Online heartbeat age stays near zero while the job goes silent | age was cached at write time | store last_heartbeat_at and compute hours at the scoring request |
| Online status moves backward after retry or same-time corrections disagree | stale or ambiguous event overwrote latest state | compare event timestamp plus source revision; quarantine unresolved ties |
| Backlog inflates during redelivery | aggregate update ran twice | deduplicate by stable event ID in same atomic write as aggregate change |
| Reliable model emits bad SLA estimates during upstream lag | freshness wasn't part of serving policy | trace age and fail to a safer response |