backend / message queues / stream processing / 01_stream_processing_fundamentals.md

Stream Processing — Windowing, Watermarks, State

7 interview angles 7 min read source

Stream Processing — Windowing, Watermarks, State

The difference between “consuming Kafka” and “doing stream processing” is windowing, watermarks, and managed state. Senior interviews probe whether you understand them.

Stream vs batch — the fundamental difference

Batch: all data is present at compute time. Bounded. Run once → finite result.

Stream: data arrives continuously. Unbounded. Computation produces evolving results over time.

Stream processing systems treat batches as a special case (“bounded streams”). The same operators that process live events can re-process historical data — same code, same semantics.

Windowing

Aggregating over an unbounded stream requires picking a window — a finite slice of the stream over which to aggregate.

Tumbling windows

Fixed-size, non-overlapping:

[0:00-0:05] [0:05-0:10] [0:10-0:15] [0:15-0:20]

Every event lands in exactly one window. Used for “events per 5 minutes,” “transactions per hour.”

Sliding windows

Fixed-size, overlapping by a sliding interval:

Window size: 10 min, slide: 5 min
[0:00-0:10] [0:05-0:15] [0:10-0:20] [0:15-0:25]

Each event lands in window_size / slide windows. “Rolling 10-minute average updated every 5 minutes.”

Session windows

Variable size — defined by gaps in activity. A new window starts after N seconds of inactivity from the previous event.

User activity gap > 30 min → new session
[User opens app for 10 min, closes, returns 1 hour later] → 2 sessions

Used for: user session analytics, fraud-detection clusters.

Hopping vs sliding terminology

“Hopping window” (Kafka Streams) and “sliding window” (Flink) refer to the same thing: overlapping fixed-size windows. The Flink-style “true sliding” recomputes the aggregate at every event (instead of at slide boundaries) — more accurate, more expensive.

Watermarks

When you say “5-minute window from 00:00 to 00:05,” when do you actually close the window and emit results?

Naive: at wall-clock 00:05. But:

  • Some events have event-time before wall-clock 00:00 (network delay, replay).
  • Some have event-time 00:04:59 but arrive at 00:05:30.

If you close at 00:05 and emit, the 00:04:59 event arriving at 00:05:30 is late and excluded.

Watermarks are a heuristic for “no more events with timestamps below this.” Stream processors emit a watermark W(t) meaning: “I won’t see any event with timestamp < t.”

When watermark exceeds the window’s end, the window closes and emits.

events:     | E(00:01) E(00:03) E(00:04:30) E(00:04:55) |
window:     [00:00 - 00:05]
watermark:  emits at e.g. 00:05:30 → "no events older than 00:05:30 expected"
                → close window, emit aggregate over events 00:01-00:04:55
late event: E(00:04:50) arriving at 00:06 → arrives after watermark → late

Handling late events

  • Drop — simplest; report dropped count for monitoring.
  • Side output — route late events to a separate stream for special handling.
  • Allowed lateness — keep the window open for N additional minutes after watermark, accepting late updates. Emits multiple results per window (updates).

Trade-off: latency vs completeness. Tight watermark = low latency, more late events. Loose watermark = high latency, fewer late events.

Event time vs processing time

Event time Processing time
Source embedded in the event payload wall clock when the processor sees the event
Reproducibility yes — same data → same windows no — depends on when processed
Latency uses watermarks immediate
Use case analytics, accuracy low-latency, monitoring

Production stream processing usually uses event time + watermarks. Processing time is fine for monitoring / metrics where exact bucketing doesn’t matter.

State

Stream operators often carry state:

  • Running totals (sum, count, avg) per group.
  • Open windows being built.
  • Session tracking per user.
  • Joins (one side waiting for the matching record on the other).

State must:

  • Persist across operator restarts.
  • Scale beyond one machine’s RAM.
  • Be consistent with input offsets for exactly-once semantics.

Stream processors provide managed state backends — typically RocksDB on local disk + periodic checkpoints to S3 / HDFS / similar.

event → operator (state lookup / update) → emit

            RocksDB local store
                  ↓ (periodic snapshot)
            S3 / durable storage

On restart, restore state from the last checkpoint, replay events since.

Exactly-once processing

The goal: each input event affects the output exactly once, even across failures.

Achieved via:

  1. Idempotent / transactional sinks. Writing to Kafka with transactional producer; writing to a DB with idempotency keys; writing to Delta Lake with checkpoints.
  2. Atomic checkpoints. State snapshot + consumer offset atomic. On restart, restore state + resume from the saved offset. No event processed twice; none lost.
  3. Distributed coordination. Chandy-Lamport-style barriers (Flink) or Kafka transactions (Kafka Streams) to align checkpoints across operators.

This is genuinely hard. Use a stream processor that provides it; don’t roll your own.

Backpressure

When downstream can’t keep up:

  • Upstream operators accumulate buffers.
  • Memory fills.
  • System OOMs.

Stream processors handle backpressure by:

  • Slowing upstream consumption (Kafka consumer lag rises, but you don’t crash).
  • Flow control between operators in the topology.

Symptom of unhealthy backpressure: consumer lag rises faster than catchup. You’re not processing as fast as data arrives; eventually you fall too far behind.

Mitigations:

  • Scale the consumer fleet.
  • Optimize the operator (fewer passes, vectorization).
  • Drop / sample under sustained load (rare; ugly).

Python options

Tool Best for Trade-offs
Kafka Streams JVM; Python via confluent-kafka (limited) not really Python
Faust Python-native, Kafka, async-first maintenance status unclear; legacy
Bytewax Rust-backed, Python API, Kafka-native newer, smaller community
PySpark Structured Streaming when already using Spark micro-batch latency (~1-10s)
Flink (PyFlink) true streaming, mature semantics JVM, complex to operate
Quix Streams Python, Kafka-focused newer

For a Python-first stream processor: Bytewax is the most credible modern option. For Spark shops: PySpark Structured Streaming. For Flink shops with Python support: PyFlink.

For truly low-latency requirements (sub-second), JVM Flink / Kafka Streams beats Python options. Python’s GIL caps throughput.

Faust example

import faust

app = faust.App("orders-stream", broker="kafka://localhost:9092")

orders_topic = app.topic("orders", value_type=Order)

@app.agent(orders_topic)
async def process(orders):
    async for order in orders.group_by(Order.user_id):
        # state-aware operation per user
        ...

Async-first, Kafka-native. Faust development has been sporadic; check current state before adopting.

Bytewax example

import bytewax.operators as op
from bytewax.dataflow import Dataflow
from bytewax.connectors.kafka import KafkaSource, KafkaSink

flow = Dataflow("orders-stream")
inp = op.input("kafka-in", flow, KafkaSource(brokers=["localhost:9092"], topics=["orders"]))
parsed = op.map("parse", inp, lambda msg: json.loads(msg.value))
windowed = op.window.tumbling_window(
    "5min",
    parsed,
    clock=op.window.EventClock(...),
    windower=op.window.TumblingWindower(length=timedelta(minutes=5)),
).reduce(...)
op.output("kafka-out", windowed, KafkaSink(...))

Rust-backed runtime; functional Python API. Promising direction for Python stream processing.

When to use stream processing

  • Real-time aggregations. Top users per hour, fraud alerts, dashboards.
  • Event-time analytics. Sessionization, funnel analysis.
  • Continuous ETL. Stream into a data lake, materialize derived tables.
  • Anomaly detection. Triggered alerts on patterns over time.

When NOT to use stream processing

  • Daily / hourly batch is fine. Simpler; cheaper; more debuggable. Don’t over-engineer.
  • Operational queries on point data. Just use a DB.
  • No real-time SLA. Stream processing infra carries a cost.

Many “streaming” projects could be cheaper as scheduled batch with minor latency cost.

Interview angle

  • “What’s the difference between batch and stream processing?” — batch: bounded data, run once, finite result. Stream: unbounded data, continuous, evolving results. Stream processors handle batch as a special case (bounded stream); same operators, same code.
  • “Tumbling vs sliding vs session windows?” — tumbling: fixed-size, non-overlapping. Sliding/hopping: fixed-size, overlapping by a step. Session: variable size, defined by gaps in activity (e.g., 30-min inactivity = new session). Pick based on what “per N time” means in your domain.
  • “What’s a watermark?” — heuristic the stream processor emits: “I won’t see events older than W(t).” When the watermark exceeds a window’s end, the window closes and emits. Trade-off: tight watermarks → low latency + more late events; loose → high latency + fewer.
  • “Event time vs processing time?” — event time: timestamp in the event payload; deterministic, reproducible, slower (needs watermarks). Processing time: when the operator sees it; immediate, not reproducible. Analytics → event time; monitoring → processing time.
  • “How does exactly-once work in stream processing?” — atomic checkpoints: state snapshot + consumer offsets committed together. On restart: restore state, resume from the offset. Plus transactional / idempotent sinks. Genuinely hard; use a system that provides it (Flink, Kafka Streams).
  • “Stream processing options for Python?” — Bytewax (Rust-backed, modern), PySpark Structured Streaming (if on Spark), Faust (legacy / sporadic maintenance), PyFlink (Flink with Python). For true low-latency, JVM Flink or Kafka Streams beat Python options.
  • “When is stream processing overkill?” — when daily / hourly batch suffices. Streaming infra has real ops cost (state backends, checkpoints, watermark debugging). Don’t add it just to have “real time” if the business doesn’t actually need it.