studyDDIAChapter 12 · Stream Processing

A Straggler Silently Undercounts a Fired Window; a Watermark Recovers It

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.


Concept

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.

Provenance

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.

Prerequisites

The surprise is when a window is allowed to decide it is done. These help; each line notes where it shows up.

  1. Tumbling windows — the stream is chopped into fixed, non-overlapping 1-minute buckets by event_time; every event falls in exactly one. It shows up as window start == the minute and the [37, 38) bucket we watch.
  2. Event-time completeness — when to fire a window — a windowed aggregate must eventually emit, but in an unbounded, out-of-order stream you can never be certain the window is complete. It shows up as the FIRE line and is the question that carries the whole result.
  3. Watermarks — a heuristic assertion "no more events with event_time < t will arrive," letting the system decide a window is complete enough to fire. It shows up as the watermark advances to … lines and the >= end fire test.
  4. Allowed lateness / drop-vs-correct policies — after firing, a window can be closed immediately (late events dropped) or kept open for a grace period (late events accepted, a correction re-emitted). It shows up as CLOSE/DROP under Policy A versus LATE … accepted / CORRECT under Policy B.
Where to learn the prerequisites Windows, watermarks, and triggers (#1–#4): DDIA §"Reasoning About Time" and §"Knowing when you're ready to trigger a window"; and Akidau et al., The Dataflow Model (VLDB 2015) — free PDF — whose what / where / when / how breakdown (windowing, watermarks, triggers, accumulation) is the canonical treatment. Watermarks specifically (#3): the "Streaming 101 / 102" articles (oreilly.com, free) walk a watermark past a window boundary with the same drop-vs-correct fork. If only one is new, make it #2 — "when is a window complete?" is the entire exercise; a watermark is just one heuristic answer, and the straggler is what that heuristic gets wrong.
Environment these numbers came from Deterministic, so the counts reproduce exactly. Captured on macOS 26.5.2 (Darwin 25.5.0), arm64, Python 3.15.0a8, standard library only — no Docker, no deps. The stream and its arrival order are modeled explicitly, so the transcript is fully deterministic — the straggler always arrives at the same point. Fixed workload: 6 events truly belong to window [37, 38) (e1–e5 on time, s1 a straggler); the watermark is the max event_time seen so far.

Mental model

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:

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.

Setup

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.


Step 1 · the window fires

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?

At what count does window 37 fire?
arrive: event e1 event_time=min 37 (watermark is -) watermark advances to 37 arrive: event e2 event_time=min 37 (watermark is 37) arrive: event e3 event_time=min 37 (watermark is 37) arrive: event e4 event_time=min 37 (watermark is 37) arrive: event e5 event_time=min 37 (watermark is 37) arrive: event e6 event_time=min 38 (watermark is 37) watermark advances to 38 FIRE window [37,38) -> count 5 (watermark 38 >= 38)

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.

Step 2 · drop-late: the straggler arrives

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?

What is window 37's final published count?
CLOSE window [37,38) (watermark 38 >= 38+0) -- stops accepting late events arrive: event e7 event_time=min 39 (watermark is 38) watermark advances to 39 FIRE window [38,39) -> count 1 (watermark 39 >= 39) CLOSE window [38,39) (watermark 39 >= 39+0) -- stops accepting late events arrive: STRAGGLER s1 event_time=min 37 (watermark is 39) DROP s1: window [37,38) is CLOSED (watermark 39 >= 38+0) -- late event ignored, published count stays 5 arrive: event e8 event_time=min 40 (watermark is 39) watermark advances to 40 FIRE window [39,40) -> count 1 (watermark 40 >= 40) CLOSE window [39,40) (watermark 40 >= 40+0) -- stops accepting late events window [37, 38) FINAL published count = 5 events that truly belong there = 6 >> UNDERCOUNT: published 5, actual 6 -- the straggler was dropped, silently, with no error.

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.

Step 3 · watermark + allowed lateness: the straggler is recovered

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?

What does the window do with the straggler?
FIRE window [37,38) -> count 5 (watermark 38 >= 38) arrive: event e7 event_time=min 39 (watermark is 38) watermark advances to 39 FIRE window [38,39) -> count 1 (watermark 39 >= 39) arrive: STRAGGLER s1 event_time=min 37 (watermark is 39) LATE s1 accepted into still-open window [37,38) (watermark 39 < 38+2) CORRECT window [37,38): published 5 -> 6 arrive: event e8 event_time=min 40 (watermark is 39) watermark advances to 40 FIRE window [39,40) -> count 1 (watermark 40 >= 40) CLOSE window [37,38) (watermark 40 >= 38+2) -- stops accepting late events

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.

Step 4 · side by side

Predict. Same nine events, same straggler, two policies. How many distinct final published counts for window 37 will the two policies produce?

How many distinct final counts do the two policies produce?
Same stream, same straggler: drop-late published 5 (undercount of 1) watermark published 6 (correct)

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.


What you should see

Why

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.

The boundary — no out-of-order arrival, or an infinite wait, and there is no straggler If events arrived in perfect event-time order, the watermark would never overtake an unarrived event, so nothing would ever be late and both policies would agree. And if you waited forever before firing (an infinitely late watermark) you would drop nothing — but never emit, which for an unbounded stream means never producing a result. The undercount bites only in the realistic middle: out-of-order arrival plus a finite wait. That is the regime every real streaming system lives in, which is why watermarks, allowed lateness, and corrections exist at all.

Go deeper

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).