Count events into 1-minute event-time tumbling windows. Window [37, 38) collects 5 events and fires at 5 when the watermark crosses minute 38. Then a straggler with a minute-37 timestamp arrives late: under the default drop-late policy it is silently dropped — published 5, actual 6, no error — but with a watermark and allowed lateness the still-open window accepts it and emits a correction, 5 → 6. Same stream, two answers.
Group a stream of events into 1-minute event-time tumbling windows and count per window. Window [37, 38) accumulates 5 events; then time moves on — events start arriving with minute-38 and minute-39 timestamps — so the aggregator declares window 37 "complete" and fires it, emitting count = 5. Then a straggler arrives: an event whose event_time is minute 37, delayed by a network hiccup so it lands after the minute-39 events. The window it belongs to has already closed. Under the default drop-late policy the straggler is silently dropped — no error — so window 37's published count stays 5 while the true count is 6: an undercount of one that nothing reports. Turn on a watermark with allowed lateness and the same window fires at 5 but is kept open for corrections; the straggler is accepted and the window emits a correction: 5 → 6. Same stream, same straggler; the only thing that changed is how long the window waits before it stops accepting late events.
Designing Data-Intensive Applications, 2nd ed. (Kleppmann & Riccomini, 2025), Chapter 12 — Stream Processing, §"Handling straggler events" (and §"Knowing when you're ready to trigger a window"). The book poses the completeness problem directly:
You can never be sure when you have received all of the events for a particular window, or whether there are some more events still to come.
Its two responses are exactly the two policies here: ignore the stragglers (dropping them, and optionally tracking a dropped-events metric), or publish a correction once the late data arrives. The exercise makes a window do both.
The surprise is when a window is allowed to decide it is done. These help; each line notes where it shows up.
window start == the minute and the [37, 38) bucket we watch.FIRE line and is the question that carries the whole result.watermark advances to … lines and the >= end fire test.CLOSE/DROP under Policy A versus LATE … accepted / CORRECT under Policy B.[37, 38) (e1–e5 on time, s1 a straggler); the watermark is the max event_time seen so far.
A window must decide when it is DONE — but in an unbounded, out-of-order stream, a late event can always arrive. There is no moment you can prove completeness, so you pick a policy:
watermark = max(event_time seen). When it reaches a window's end, the window fires. An event is late if its window's end is already ≤ the watermark — the watermark had already called that window complete.grace more minutes of watermark. A late event that arrives while open is accepted, and the window re-emits a correction. Higher completeness, at the cost of holding state longer and emitting updates.The trade is latency vs completeness: wait longer (or keep windows open longer) and you drop fewer stragglers but publish later. Both policies run the identical stream through one aggregator; the only difference is the grace period. The whole thing is one script, code/straggler_watermark.py.
cd study/ddia/ch12/code
python3 straggler_watermark.py
The stream is fixed. Nine events arrive in this processing order — note s1, a minute-37 event that shows up out of order, after the minute-39 event:
arrival order (processing time): e1@min37, e2@min37, e3@min37, e4@min37, e5@min37, e6@min38, e7@min39, s1@min37, e8@min40
events that TRULY belong to window [37, 38): 6 (e1 e2 e3 e4 e5 s1)
Do not read the output yet — make each prediction first.
Predict. Window [37, 38) collects e1–e5, all with event_time minute 37. Then e6 arrives with event_time minute 38, pushing the watermark to 38. The aggregator now declares window 37 complete and fires it. Six events truly belong to this window — so at what count does it fire?
Fires at 5, not 6. Only 5 of the window's 6 events had arrived when the watermark crossed the window's end. The watermark said "no more minute-37 events are coming" — and it was wrong, but the window had no way to know that. It fires optimistically at 5.
Predict. Now s1 arrives with event_time minute 37 — but the watermark is already at 39, so its window is in the past. Under the default drop-late policy (the window closed the moment it fired), what happens to s1, and what is window 37's final published count?
Published 5, actual 6 — dropped with no error. The window closed the instant it fired (watermark 38 >= 38+0), so when s1 shows up it hits a closed window and is discarded. No exception, no warning; window 37 is permanently one short.
Predict. Same stream, same straggler — but now the window carries a grace of 2 minutes: after firing it stays open until the watermark passes its end plus the grace. When s1 arrives (watermark 39), window 37's end + grace is 38 + 2 = 40, still ahead of the watermark. What does the window do with s1?
Corrected to 6. The window fired on time at 5, exactly as before — but this time it did not close. Because watermark 39 < 38+2, window 37 was still open when s1 arrived, so s1 was accepted and the window re-emitted a correction, 5 -> 6. Only when e8 pushes the watermark to 40 (>= 38+2) does window 37 finally close.
Predict. Same nine events, same straggler, two policies. How many distinct final published counts for window 37 will the two policies produce?
Two different answers — 5 and 6 — from the identical stream. The straggler never changed; what changed is whether the window was still willing to hear it. Drop-late traded completeness for latency and lost the event; the watermark with allowed lateness held the window open one more beat and recovered it.
In an unbounded, out-of-order stream you can never be certain a window is complete: at any moment a delayed event with an old timestamp might still be in flight. So a windowed aggregator cannot wait for proof — it must pick a policy for deciding "complete enough," and every policy is a bet. The watermark is that bet made explicit: "no more events before t." When it crosses a window's end the window fires, and it fires with whatever has arrived — here, 5. The straggler is dropped precisely because the window already closed on an optimistic watermark: the assertion "no more minute-37 events" turned out false, but the window had already acted on it. Nothing errored because dropping late data is not a failure in this model — it is the policy, working as designed.
Allowed lateness changes the bet without changing the watermark. The window still fires on time (same latency for the common case), but instead of closing it keeps its state and stays open for a grace period, so a late arrival is folded in and the result re-emitted as a correction. You have moved along the latency vs completeness curve: same fast first answer, plus a slower, more-complete follow-up, paid for by holding window state longer and by making every downstream consumer tolerate updates. The correction only helps if the straggler arrives within the grace — a later straggler still hits CLOSE and is dropped, which is why the grace is a tuning knob, not a guarantee.
Sources: DDIA 2e, Ch. 12, §"Handling straggler events", §"Knowing when you're ready to trigger a window" · Akidau et al., "The Dataflow Model" (VLDB 2015).