Search Tech Journey

Find topics, journeys and posts

6-month learning plan49 / 130
back to blog
data engineeringintermediate 15m read

S049 · Stream Processing — Watermarks, Windows, Exactly-Once

Time is the hard part of streaming.

Module M05: Data Engineering · Session 49 of 130 · Track: DE ⏱️

What you'll be able to do after this session

  • Explain Stream Processing to a friend in one minute, out loud, no notes.
  • Recognise it when you see it in real code / systems / papers.
  • Complete the hands-on exercise at the end without looking up help.

Prerequisites

If you can't answer these in 30 seconds each, redo the linked session first:


Pre-read (skim before the session)


(a) Intuition · 5 min

The one-paragraph mental model. Why this concept exists, what problem it solves, and the analogy that makes it stick.

In one sentence: Time is the hard part of streaming.

You're on a train, watching messages arrive on your phone. Someone's phone was in a tunnel for 10 minutes — their "I'll be at the station at 3pm" message just landed at 3:07pm. Is that message "late" or "on time"? From the message's point of view (it was sent at 2:55pm), it's on time. From your point of view (it arrived at 3:07pm), it's late. Now imagine you're running a "what happened between 3pm and 3:05pm?" report and you get that message at 3:07pm — do you include it? These are the questions stream processing exists to answer, and the answer is subtle enough that it took the industry 15 years and a dedicated Google paper (the Dataflow Model) to settle.

The core distinctions: event time is when the event happened at the source. Processing time is when your stream engine sees it. In batch, these are basically the same; in streaming, they can differ by seconds (network jitter), minutes (a laptop was offline), or days (a phone was in airplane mode). A window is a slice of the stream you're computing over: "5-minute tumbling window," "24-hour sliding window every hour," "session window per user." A watermark is the engine's best guess of "we've probably seen all events with event-time ≤ X." When the watermark passes a window's close, the window fires and emits a result — but late data may still arrive.

The gotcha to carry forever: streaming makes you decide, explicitly, what "correct" means when time is fuzzy. Do you emit at watermark and never update? Emit early speculative results and update? Wait a long time for late data at the cost of latency? "Exactly-once" is not a single knob — it's a chain (source, engine state, sink) and each link must be idempotent for the whole thing to hold. Beginners write streaming code assuming events arrive in order, at processing time, and the world matches their mental model. It never does.


(b) Visual walkthrough · 15 min

Diagrams, worked examples, step-by-step visuals.

The three "kinds of time" you must always distinguish:

Window types, in one table:

WindowShapeExample use
TumblingFixed size, non-overlapping"Sales per 1-min bucket"
SlidingFixed size, overlaps by a step"Sales in the last 5 min, updated every 30 sec"
SessionDynamic gap between events per key"User's browsing session"
GlobalOne big window, custom triggersContinuous aggregations with early emits

Worked example — 1-minute tumbling window, watermark = event_time − 10s:

Events (event_time → processing_time):
  A@10:00:03 → arrives 10:00:04
  B@10:00:07 → arrives 10:00:08
  C@10:00:15 → arrives 10:00:16
  X@10:00:05 → arrives 10:01:20   ← LATE! (arrived 75s after event)
  D@10:01:02 → arrives 10:01:03

The window [10:00:00, 10:01:00) normally fires when watermark reaches 10:01:00 (i.e., processing time ≈ 10:01:10). It emits {A, B, C} and closes. When X arrives at 10:01:20, it targets the closed window — you now choose:

  1. Drop late data (simplest, some data loss).
  2. Send to a side-output for a nightly reconciliation job.
  3. Configure allowedLateness=5min — the window re-fires with {A, B, C, X} — but downstream sinks must be idempotent so {A,B,C} isn't double-counted.

Exactly-once — the chain:

Break any link and you get at-least-once + duplicates. Flink's answer: distributed checkpoints (a coordinated snapshot of all operator state + Kafka offsets), plus transactional sinks (Kafka, Iceberg, JDBC with 2PC) so state and output move forward atomically. Spark Structured Streaming does the equivalent with a write-ahead log per micro-batch. Kafka Streams uses transactions to commit offsets + output atomically.


(c) Hands-on · 20 min

Code you type and run. Not pseudo-code — real, runnable, copy-pasteable.

Simulate late events and a tumbling window in pure Python (no Flink install needed).

# stream_lab.py — a mini stream processor demonstrating event time, watermarks, and windows
import heapq, collections, time, random
 
WINDOW_SIZE     = 10           # seconds (tumbling)
ALLOWED_LATENESS = 5           # seconds — accept late events this old
WATERMARK_DELAY  = 3           # emit when processing time ≥ window_end + this
 
# Fake event stream. Each event: (event_time, key, value).
# Some events are late (event_time much less than "now").
def event_source(n=100):
    now = 1000
    for i in range(n):
        now += random.random() * 2                 # advance ~1s per tick
        # 20% of events are "late" by 4-8s
        if random.random() < 0.2:
            et = now - random.uniform(4, 8)
        else:
            et = now - random.uniform(0, 1)
        yield (et, random.choice(["A","B","C"]), 1)
        time.sleep(0.02)
 
# Per-key window state: {(window_start, key): count}
state = collections.defaultdict(int)
emitted_windows = set()
 
max_event_time = 0
for event_time, key, val in event_source():
    max_event_time = max(max_event_time, event_time)
    watermark = max_event_time - WATERMARK_DELAY
 
    win_start = int(event_time // WINDOW_SIZE) * WINDOW_SIZE
    win_end   = win_start + WINDOW_SIZE
 
    # accept only if window is still open (or within allowed lateness)
    if win_end + ALLOWED_LATENESS < watermark:
        print(f"  DROPPED late event et={event_time:.1f} key={key} (win closed)")
        continue
 
    state[(win_start, key)] += val
 
    # fire any windows whose end <= watermark and haven't been fired yet
    for (ws, k), count in list(state.items()):
        if ws + WINDOW_SIZE <= watermark and (ws, k) not in emitted_windows:
            print(f"WIN [{ws:.0f}-{ws+WINDOW_SIZE:.0f}) key={k} count={count}  (watermark={watermark:.1f})")
            emitted_windows.add((ws, k))

Run it:

python3 stream_lab.py

Observe (checklist):

  1. Each window fires only after the watermark has advanced past window_end — that's the delay streaming pays for correctness.
  2. Watermark = max event time seen − 3s — a heuristic. Real engines (Flink) let you plug in custom watermark strategies.
  3. Some events with et far in the past get printed as DROPPED — they missed the window even including allowed lateness.
  4. Even the "on-time" events can arrive in slightly different processing-time order — event time is what matters for the window.
  5. Windows fire once, so downstream must not re-count them — this is why sinks must be idempotent.

Try this modification: re-emit updated results when late events arrive (change emitted_windows from "set of fired windows" to "map from window → last count," and always emit an update when count changes). You've just implemented Flink's "accumulating" trigger mode — and now understand why downstream sinks must be idempotent upserts, not blind inserts.


(d) Production reality · 10 min

How this shows up in real systems, gotchas, war stories, common bugs.

The most famous public streaming story: Alibaba processes ~4 billion events/sec at peak during Singles' Day (Nov 11) using Flink. Their engineers have published multiple papers on what breaks at that scale — mostly checkpointing overhead and RocksDB state size. Lesson: state is the hard part of streaming, not throughput. A stateless map-filter pipeline scales trivially. A stateful join or session-window pipeline has state proportional to your key cardinality × window size, and eventually every state store (RocksDB in Flink, HDFS/S3 in Spark SS) hits limits. Rule of thumb: keep window sizes bounded (session-based, or with hard TTLs), and shard state by a high-cardinality key.

Watermark tuning is where you lose sleep. Too aggressive (small delay) → too much late data dropped, wrong results. Too conservative (large delay) → dashboards are minutes behind reality, angry PMs. Uber's answer: dual-tier — an "early speculative" window that emits at low latency, plus a "final" window with generous lateness that overwrites the speculative result. Downstream stores must support this (upsert semantics). Confluent's ksqlDB handles this with EMIT CHANGES.

Exactly-once end-to-end is a bar most teams don't clear. In practice, at-least-once + idempotent sinks (dedup by an event ID or upsert by primary key) is far more robust and much cheaper. Netflix's Keystone explicitly chose at-least-once for its scale. Reserve true exactly-once for financial ledgers and billing systems, where the extra cost is justified.

Failure recovery is where streaming systems earn their salary. A Flink checkpoint failure that leaves state in an inconsistent state means restoring from an older checkpoint and replaying Kafka from that point. If Kafka's retention was too short, you cannot recover — permanent data loss. Set Kafka retention to at least 3-7 days for topics feeding stateful streaming jobs, and monitor checkpoint duration + size — a checkpoint that takes 4 minutes is a smell that your state is too big.

Finally: backpressure is a feature, not a bug. When your sink is slow, Flink slows down consumption from Kafka. This is desirable — it prevents runaway memory growth. Alert on it; don't fix it by adding buffers.


(e) Quiz + exercise · 10 min

  1. Explain the difference between event time, processing time, and ingestion time. Which one should windowing use, and why?
  2. What is a watermark and what trade-off do you make when you set its delay large vs small?
  3. Compare tumbling, sliding, and session windows with one use case for each.
  4. What are the three "links in the chain" you must control to achieve end-to-end exactly-once processing?
  5. Why is idempotent output usually preferred over transactional exactly-once sinks in real production?

Stretch (connects to S048 — Kafka): you're building a "revenue in the last hour, updated every 30 seconds" dashboard on top of Kafka. Sketch the full architecture: partition key on the topic, windowing choice, watermark strategy, sink store, and what happens when a bug requires re-running the last 24 hours' worth of results.


Explain-out-loud test

If you can't teach these 3 things to a friend without notes, redo the session:

  1. What is Stream Processing? (one sentence, no jargon)
  2. When would you use it? (one concrete example)
  3. When would you NOT use it? (one anti-pattern)

What comes next


Part of a 130-session evergreen learning series. Session structure: (a) intuition · (b) visual walkthrough · (c) hands-on · (d) production reality · (e) quiz + exercise. Duration: 60 minutes.

More from M05 · Data Engineering

all modules →
  1. 045Data Modelling — Dimensional, Data Vault, OBT
  2. 046Batch vs Streaming — Mental Model & Use Cases
  3. 047Spark — RDD, DataFrame, Jobs/Stages/Shuffles
  4. 048Kafka — Topics, Partitions, Consumer Groups
  5. 050Orchestration — Airflow, DAGs, Retries, Backfills
  6. 051dbt — Models, Tests, Docs, Warehouse-Native ELT