State and Stateful Stream Processing
Simple stream operations — filter this event, transform that one — need no memory. But the interesting computations — counts, aggregations, joins, windows, pattern detection — require remembering things across events, and that memory is state. Managing state correctly over an unbounded stream, at scale, with fault tolerance, is the hardest engineering problem in stream processing. This post is about state: why it's needed, what makes it hard, and how stream processors manage it.
Posts 2–4 built events, time, and windows. This post is about what windows and aggregations implicitly require: state — the memory a stream processor keeps across events. It’s the dividing line between trivial and powerful stream processing, and the foundation that fault tolerance (post 6) must protect.
Stateless vs stateful
Stream operations split cleanly into two kinds:
- Stateless operations process each event independently, needing no memory of other events: map (transform an event), filter (keep/drop based on the event alone), simple routing. Each event in, each event out, with no reference to anything else. These are easy — trivially parallelizable, nothing to remember, nothing to lose on failure.
- Stateful operations need to remember information across events: aggregations (a running count/sum needs the accumulated total so far), windows (a window accumulates its events until it emits — post 4), joins (matching events from two streams requires remembering events from one while waiting for the other), deduplication (remembering what you’ve seen), and pattern/sequence detection (detecting “A then B then C” requires remembering you saw A and B). These require state — stored memory that persists across events and is updated as events arrive.
Almost everything interesting in stream processing is stateful. Counting, aggregating, windowing, joining, detecting patterns, enriching events with remembered context — all need state. So while stateless operations are simple plumbing, state is where the real power and the real difficulty live. The defining property of a serious stream processor is how well it manages state, and most of what makes stream engines sophisticated (and hard to build) is their state machinery.
Why state in streaming is hard
State would be easy if the stream were small and nothing ever failed. On a real, unbounded, high-volume stream running forever, state is genuinely hard, for several compounding reasons:
- It’s unbounded-by-default, so it must be bounded. A naive “remember everything” state grows without limit on an infinite stream (post 1) — you’d remember every event forever and run out of memory. So state must be bounded: windows expire and release their state when they close (post 4), old entries are evicted, time-to-live policies drop stale state. Managing what to keep and what to forget on an infinite stream is a core discipline — unbounded state is the classic way streaming jobs crash.
- It must be fast and local. Stateful operations update state per event, at the stream’s full throughput (potentially millions of events/second), so state access must be extremely fast — typically kept local to each processing node (in memory, or a fast embedded store) rather than in a remote database that would bottleneck every event. State locality is what lets stateful processing keep up with the stream.
- It must be partitioned to scale. To process a high-volume stream in parallel, you partition it across many nodes (usually by key — e.g. by user ID), and each node holds the state for its keys. This keyed state is how stateful processing scales horizontally: partition the stream by key, and each partition’s state lives with its partition. Getting partitioning right (so related events and their state land together) is central to scalable stateful streaming.
- It must survive failure — the hard part (post 6). Here’s the crux: the state is the accumulated result of processing possibly billions of past events, so if a node crashes and loses its state, you can’t just “reprocess from the start” of an infinite stream. Losing state means losing the computation’s memory, which is often catastrophic. So state must be made fault-tolerant — durably checkpointed so it survives crashes — which is the central problem of post 6 and the reason stream processing fault tolerance is all about state.
The through-line: state is what makes stream processing powerful and what makes it hard. Simple stateless streaming is easy; the moment you need to remember anything across events (which is almost always), you inherit the problems of bounding, speed, partitioning, and — above all — surviving failure.
How stream processors manage state
Modern stream engines (Flink especially) provide sophisticated managed state so you don’t solve all this yourself, and understanding the shape of it clarifies how real streaming systems work:
- Keyed state, partitioned with the stream. The engine lets you keep state per key (per user, per device), automatically partitioned so each parallel instance owns its keys’ state — giving scalable, local, fast state access. You write “update this user’s running count”; the engine handles where that state lives and how it’s partitioned.
- Local, embedded state backends. State is held in fast local stores (in-memory for speed, or an embedded on-disk store like RocksDB for state too large for memory), so per-event access is fast and doesn’t bottleneck on a remote database. Large state (too big for RAM) spills to local disk transparently.
- Managed lifecycle (bounding). The engine ties state to windows and provides time-to-live / eviction so state is bounded and expired automatically (post 4’s windows releasing state) — relieving you of the “remember forever” trap, though you must still design TTLs and window sizes sensibly.
- Checkpointing for fault tolerance (post 6). The engine periodically, consistently snapshots all this distributed state durably, so on a crash it can restore and resume — the mechanism that makes stateful streaming survivable, covered next.
The key insight for an engineer: in modern stream processing you declare stateful logic (keyed aggregations, windows, joins) and the engine provides the hard machinery — partitioned keyed state, fast local backends, bounded lifecycle, and fault-tolerant checkpointing. This is a big part of why you use a stream-processing framework rather than rolling your own: the framework’s value is overwhelmingly in correctly managing state at scale with fault tolerance, which is brutally hard to build yourself. Stateless streaming you could hack together; stateful, scalable, fault-tolerant streaming is exactly what the frameworks exist to provide.
The throughline: stream operations are stateless (process each event independently — map/filter/route; easy, parallelizable, nothing to lose) or stateful (remember across events — aggregations, windows, joins, dedup, pattern detection; require state). Almost everything interesting is stateful, so state is where stream processing’s power and difficulty both live. State is hard on a real stream because it’s unbounded-by-default (must be bounded via windows/TTL/eviction or the job crashes), must be fast and local (updated per event at full throughput — kept in-node, not a remote DB), must be partitioned by key to scale (keyed state — each node owns its keys’ state), and above all must survive failure (state is the accumulated result of billions of events — you can’t reprocess an infinite stream, so losing it is catastrophic — the central problem of post 6). Modern engines provide managed state — partitioned keyed state, fast local/embedded backends (RocksDB), bounded lifecycle, and checkpointing — which is the main reason to use a framework rather than roll your own.
Key takeaways
- Operations are stateless (process each event independently — map/filter/route; easy, trivially parallel, nothing to remember or lose) or stateful (remember across events — aggregations, windows, joins, deduplication, pattern/sequence detection; require state persisted and updated per event).
- Almost everything interesting is stateful, so state is where stream processing’s power and difficulty live — how well an engine manages state is what makes it serious.
- State is hard on a real unbounded stream: unbounded-by-default (naive “remember everything” grows without limit → must bound via window expiry, TTL, eviction — unbounded state is the classic crash), must be fast and local (per-event updates at full throughput → kept in-node, not a bottlenecking remote DB), must be partitioned by key to scale (keyed state — each node owns its keys’ state), and must survive failure (state = the accumulated result of billions of past events; you can’t reprocess an infinite stream, so losing it is catastrophic — the crux of post 6).
- Modern engines provide managed state: keyed state auto-partitioned with the stream (scalable, local, fast), local embedded backends (in-memory for speed, on-disk like RocksDB for large state), managed lifecycle (TTL/eviction tied to windows — bounding for you), and checkpointing for fault tolerance (post 6).
- You declare stateful logic and the engine provides the hard machinery — which is why you use a stream framework at all: its value is overwhelmingly correct, scalable, fault-tolerant state management, which is brutal to build yourself (stateless streaming you could hack together; stateful scalable fault-tolerant streaming is what frameworks exist for).
Further reading
- Stream processing — stateful operations over streams
- State (computer science) — the memory stateful processing keeps