How stream processing engines work
Open in the interactive app: practice, quiz, mock interview →In one minute
- A streaming engine (Flink, Kafka Streams, Spark Structured Streaming, Google Dataflow) runs long-lived, stateful computations over unbounded event streams.
- Results are grouped into windows by event time, and watermarks tell the engine when a window is probably complete so it can emit a result.
- State (counts, joins, sessions) lives next to the computation, partitioned by key, and is periodically checkpointed together with input offsets.
- Exactly-once results come from replaying input from the last consistent checkpoint plus transactional or idempotent sinks, not from never failing.
- The hard operational problems are state size, backpressure, skewed keys, and late data.
How it works
Dataflow graph and parallelism
A job is a graph of operators, each running as N parallel tasks. keyBy(user_id) hash-partitions records so every event for a key reaches the same task, which owns that key's state. Rescaling moves state in units of key groups, a fixed set of hash buckets assigned to tasks in ranges. The number of key groups is the job's max parallelism, which caps how far it can ever scale out without discarding state (Flink config).
Event time, windows, and watermarks
Events carry the time they happened, which differs from arrival time when phones go offline or partitions lag. A tumbling window groups fixed, non-overlapping intervals (every minute); a sliding window overlaps (5 minutes every minute); a session window closes after a gap of inactivity. A watermark W(t) is a claim that no more events with timestamps at or before t are expected. Sources usually generate it as "max seen timestamp minus allowed out-of-orderness", for example 30 seconds. An operator with several inputs takes the minimum of their watermarks (Flink docs). When the watermark passes a window's end, the window fires. Events arriving after that are late: they can update the result within an allowed-lateness period, go to a side output, or be dropped.
State and checkpoints
State lives in memory (Flink's default heap backend) or in an embedded RocksDB store on local disk, which handles terabytes per job. Flink 2.0 (March 2025) added a third option, the ForSt backend, which keeps its SST files on remote storage such as S3 and uses local disk only as a cache, so state is no longer bounded by a node's disk and rescaling moves less data (Flink 2.0). The docs still mark it experimental and warn about network latency when hot state is remote (Flink docs). Flink takes consistent snapshots using barriers: the source injects a marker into the stream; each operator, on receiving the barrier from all inputs, snapshots its state to durable storage and forwards the barrier. When all sinks acknowledge, the checkpoint is complete and includes source offsets. On failure, every task restores the last checkpoint and sources rewind to its offsets.
Exactly-once end to end
Replay alone produces duplicates in external systems. Sinks therefore either write idempotently (upsert by key) or use two-phase commit: write into a transaction during the checkpoint interval and commit only when the checkpoint completes. The latter adds output latency equal to the checkpoint interval.
Down the stack
Layer 1The contract
With checkpointing enabled and a replayable source, the engine promises that operator state reflects each input record exactly once after recovery. With a transactional or idempotent sink, that extends end to end. Window results are emitted once the watermark passes the window end, and the engine promises nothing about records that arrive after the allowed lateness except that they will not silently change already-final results. Latency is bounded by the watermark delay plus processing time, and with transactional sinks also by the checkpoint interval.
Layer 2The mechanism
In Flink, keyed state is split into key groups, the number set by maxParallelism. Unless you set it, Flink derives it as roughly 1.5 times the operator's parallelism rounded up to a power of two, with a floor of 128 and a ceiling of 32,768 (Flink docs); set it explicitly, because changing it later breaks state compatibility. Each key maps to murmurHash(key) % maxParallelism, and each task owns a contiguous range of key groups, so rescaling moves whole ranges. With the RocksDB backend, each state entry is stored under a composite key of key group, key, and namespace (the window), so all state for one window can be found and cleared by prefix. Timers for window ends live in a priority queue per task, ordered by timestamp; when a watermark arrives, the task pops every timer at or before it and fires those windows. A checkpoint barrier carries a checkpoint ID; an operator with several inputs aligns by buffering inputs that already delivered barrier n until all inputs have, then snapshots. Unaligned checkpoints skip that wait by including in-flight buffers in the snapshot. Incremental RocksDB checkpoints upload only new SST files since the last checkpoint, but they are off by default (execution.checkpointing.incremental: false) (Flink config), so large-state jobs must turn them on.
Layer 3The machine
RocksDB is a log-structured merge tree: writes go to an in-memory memtable (64 MB by default) and a write-ahead log, flush to immutable SST files, and are merged by background compaction. Point reads that miss the block cache cost an SSD read of about 100 microseconds, so a job doing random state reads at 100,000 per second per task needs either a large block cache or fast local NVMe; network-attached disks with 1 to 2 ms latency will backpressure the job. Snapshots are cheap because SST files are immutable: the checkpoint hard-links current SST files and uploads them asynchronously, so processing barely pauses. Records cross tasks through network buffers of 32 KB (taskmanager.memory.segment-size); when the receiver has no free buffers, credit-based flow control stops the sender, which is how backpressure propagates without dropping data.
Why it's built this way
- Events arrive out of order, so results are defined by event time and a watermark estimates completeness.
- State is too large for memory and must survive failure, so it lives in an embedded LSM store and is snapshotted to durable storage.
- Pausing the world to snapshot would add latency, so barriers flow with the data and snapshots are asynchronous.
- External systems cannot be rolled back, so end-to-end exactly-once needs idempotent or two-phase-commit sinks.
Diagram
sequenceDiagram participant S as Source participant O as Window operator participant K as Sink participant C as Checkpoint store S->>O: events with event time S->>O: watermark 12:00:30 O->>K: fire window 11:59-12:00 S->>O: checkpoint barrier n O->>C: snapshot state for n O->>K: forward barrier n K->>C: pre-commit transaction C-->>K: checkpoint n complete K->>K: commit transaction
Worked example
A ride-hailing service computes surge pricing per geohash cell every minute from driver and rider pings.
Input: 400,000 pings per second, keyed by roughly 250,000 active cells. Each cell keeps a one-minute window of about 200 bytes, so live state is around 50 MB. The real load is update rate: 400,000 updates per second over 48 tasks is about 8,300 per task, comfortable for RocksDB.
Lateness: 99 percent of pings arrive within 5 seconds, 99.9 percent within 20 seconds. With a 20-second out-of-orderness bound, a window ending at 12:01:00 fires around 12:01:20, so surge prices are about 20 seconds stale. A 5-second bound is fresher but makes 1 percent of events late; those go to a side output and prices are not revised.
Checkpoints every 30 seconds write about 200 MB incrementally to object storage in 2 seconds. A task failure costs at most 30 seconds of reprocessing plus restore, around 45 seconds total, during which prices hold their last value. The sink upserts by (cell, window_start), so replayed results overwrite rather than duplicate.
In the wild
- Jepsen, 2024: the two-phase-commit sink is only as good as the broker. Testing Bufstream, a Kafka-compatible broker, Jepsen found that Kafka's transaction protocol has no sequence number on commit and abort messages. A delayed
EndTxnfrom one transaction could commit or abort the producer's next transaction, giving lost writes, aborted reads and torn transactions, and Jepsen reproduced it in Kafka itself under process pauses (KAFKA-17754). KIP-890, which bumps the producer epoch on every transaction, is the expected fix. Engines that rely on Kafka transactions for exactly-once output inherit this (Jepsen). - Cloudflare, 2024: per-key buffers meet a 40x fan-out. A bug produced a blank Logpush configuration and was reverted within five minutes, but Logfwdr treated "no config" as a reason to fail open and forwarded logs for every customer. Buftee, which keeps one buffer per customer, saw about 40 times its usual buffer count; its overload protections had never been configured, it stopped responding, and about 55% of customer logs were lost over roughly 3.5 hours. Cloudflare added misconfiguration alerts and regular overload tests (Cloudflare).
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Apache Flink | Event-at-a-time, rich windows, large state, mature checkpoints | Cluster to operate, steep learning curve | Complex stateful jobs at high volume |
| Kafka Streams | A library in your service, no cluster | Kafka-only; slow state rebuild from changelog | Stateful microservices on Kafka |
| Spark Structured Streaming | Same API as Spark batch; micro-batches | Latency typically 100 ms to seconds (Real-time Mode in 4.1 goes lower, stateless queries only); less flexible windows | Teams already on Spark, seconds-level latency fine |
| Managed (Google Dataflow, Amazon Managed Service for Apache Flink) | No cluster ops, autoscaling | Cost, vendor lock-in, less tuning control | Small platform teams |
Amazon Managed Service for Apache Flink is the August 2023 name for what AWS called Kinesis Data Analytics; the APIs did not change, so older guides and IAM policies still use the old name (AWS).
Pitfalls in production
- Stuck watermarks. One idle Kafka partition has no events, so the minimum watermark never advances and no window fires. Configure idle-source timeouts (
withIdlenessin Flink) so idle partitions are excluded, and watermark alignment so one fast source cannot run far ahead and bloat downstream buffers (Flink docs). - Unbounded state. A join or dedupe without TTL keeps every key forever until disks fill. Set state TTL and use windowed joins.
- Hot keys. A single key (a big customer, a null key) overloads one task and backpressures the job. Pre-aggregate with a salted key and merge, or filter bad keys at the source.
- Checkpoints timing out. Under backpressure, barriers queue behind data and checkpoints fail, so recovery points grow old. Fix the bottleneck, enable unaligned checkpoints, or reduce state per checkpoint with incremental snapshots.
- Non-idempotent sinks. Restarts create duplicate rows in the database. Upsert by deterministic keys or use transactional sinks.
Leadership lens
- Ask for the lateness distribution of the input before accepting a watermark setting. The bound is a product decision about freshness versus completeness.
- Require state size estimates and TTLs in design docs, plus a plan for upgrading jobs with savepoints.
- Monitor consumer lag, checkpoint duration, and watermark delay as first-class SLOs.
- Prefer the simplest engine that meets the latency target; a library may avoid running a cluster.
Check yourself
Why does one idle partition stop all windows from firing?
Show answer
The operator's watermark is the minimum across its inputs, and an idle partition never advances its watermark, so the minimum stays stuck.
How do barriers produce a consistent snapshot without pausing the whole job?
Show answer
Each operator snapshots its state when the barrier passes, so every snapshot reflects exactly the events before that barrier, and together with source offsets they form a consistent cut.
What does a shorter out-of-orderness bound trade away?
Show answer
Results arrive sooner, but more events become late and are either dropped, handled separately, or cause revisions to emitted results.
Sources and further study
Primary
- Akidau et al., MillWheel (VLDB 2013): persistent state, logical time and framework-managed fault tolerance in Google's production engine.
- Armbrust et al., Structured Streaming (SIGMOD 2018): the declarative, incrementalised-query alternative to building an operator graph by hand.
Systems docs
- Flink: generating watermarks: bounded out-of-orderness, idleness and watermark alignment.
- Flink: state backends: heap, RocksDB and ForSt, and when each fits.
- Kafka Streams documentation: the library model, with state stores backed by changelog topics instead of a cluster.
Books
- Streaming Systems, Tyler Akidau, Slava Chernyak and Reuven Lax, chapters 3 "Watermarks", 5 "Exactly-Once and Side Effects" and 7 "The Practicalities of Persistent State".
- Stream Processing with Apache Flink, Fabian Hueske and Vasiliki Kalavri, chapters 3 "The Architecture of Apache Flink" and 7 "Stateful Operators and Applications".
- Designing Data-Intensive Applications, Martin Kleppmann, chapter 11 "Stream Processing", section "Processing Streams" (chapter 12 in the 2026 second edition).
Other curricula
- Azure Architecture Center: big data architectures: places stream processing as the hot path behind a buffering ingestion layer such as Event Hubs or Kafka.
Watch
- Fundamentals of stream processing with Apache Beam (Tyler Akidau): windows, watermarks, and triggers from one of the Dataflow authors.
- Event Time and Watermarks (Apache Flink 101, Confluent): a concise explanation of how Flink tracks event time.
Free resources
- The Dataflow Model (VLDB 2015): the paper defining windowing, triggers, and watermarks as used by modern engines.
- Streaming 102 (Tyler Akidau): worked examples of watermarks, triggers, and accumulation modes.
- Lightweight Asynchronous Snapshots for Distributed Dataflows: the barrier-based checkpointing algorithm behind Flink.
- Flink stateful stream processing concepts: keyed state, checkpoints, and exactly-once in the official docs.