Stateful vs. Stateless Stream Processing in Apache Flink
Apache Flink
5 MIN READ
August 5, 2026
![]()
A fraud-detection system that blocks a stolen card needs to know what happened on that card seconds earlier. A log-cleaning pipeline that strips out malformed events doesn’t need to know anything at all. Both are “stream processing,” but they demand fundamentally different architectures.
This is the choice every real-time data engineer eventually has to make: should a processing node carry memory of past events, or should it treat each one as a self-contained, disposable unit of work? Get it wrong in one direction, and you bolt on checkpointing, state backends, and recovery machinery your pipeline never actually needed. Get it wrong in the other direction, and you ship metrics that are quietly inaccurate because the system never saw the bigger picture.
That distinction, stateless vs. stateful stream processing, is the architectural fork this blog walks through, using Apache Flink as the reference implementation.
In this blog, we’ll:
- Define both paradigms and explain why the distinction matters in production
- Unpack the engineering trade-offs: operational complexity, memory dynamics, and recovery mechanics
- Compare real Apache Flink code for stateless vs. stateful implementations
- Cover state backend selection, TTL, and skew, the pitfalls that actually break pipelines at scale
- Close with a decision framework you can apply to your own architecture
Key Difference Between Stateless Stream Processing and Stateful Stream Processing
Stateless Stream Processing
In a stateless architecture, each event is processed in complete isolation. The engine reads an event, applies a transformation or filter, and emits the result, with zero memory of anything that came before it.
Because no operator instance depends on another, stateless pipelines scale horizontally with almost no coordination overhead. Spin up ten parallel instances, partition the data arbitrarily across them, and each instance processes its share correctly, with no cross-instance communication required.
Stateful Stream Processing
Stateful processing requires the engine to retain information across multiple events. The outcome for the current event depends on a history of prior events tied to the same key.
Stateful processing is what makes complex real-time analytics possible, but it comes with lifecycle management responsibilities that stateless pipelines simply don’t have.
Visualizing the Memory Lifecycle
The contrast in memory management overhead between these two architectures becomes clear when you compare their layout profiles side by side.
Stateful processing is non-negotiable for use cases like:
- Calculating a moving average over a sliding time window
- Detecting a sequence of fraudulent login attempts across multiple events
- Joining two independent real-time streams on a shared key
- Deduplicating events within a defined time boundary
Architectural Trade-Offs: Operational Overhead vs. Contextual Power
This choice ripples through your infrastructure footprint, capacity planning, and disaster recovery design.
- The Stateless Profile: Minimal operational surface area. A crashed node is replaced with zero recovery orchestration, since there’s nothing to restore. Latency is the lowest achievable, since no thread ever blocks on a state-backend lookup. The trade-off: scope is limited to filtering, format conversion, enrichment from external lookups, and basic validation.
- The Stateful Profile: Unlocks real-time aggregation, windowing, joins, and pattern detection that drive operational decisions, like blocking a fraudulent transaction within milliseconds. The cost is real engineering investment: choosing and tuning a state backend (RocksDB vs. heap-based), configuring checkpointing intervals, planning for state schema evolution across deployments, and monitoring state size growth.
Code Comparison: Stateless vs. Stateful in Apache Flink
Consider a stream of sensor readings with two requirements:
- Stateless: Filter out any reading below 0°C.
- Stateful: Alert when a sensor’s temperature rises more than 10°C compared to its previous reading.
1. Stateless Implementation: Filtering
import org.apache.flink.api.common.functions.FilterFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class StatelessPipeline {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<SensorReading> stream = env.addSource(new SensorSource());
// Stateless Filter: Evaluates each event entirely in isolation
DataStream<SensorReading> filteredStream = stream.filter(new
FilterFunction<SensorReading>() {
@Override
public boolean filter(SensorReading reading) {
return reading.getTemperature() >= 0.0;
}
});
filteredStream.print();
env.execute("Stateless Sensor Filter");
}
}
2. Stateful Implementation: Delta Alerting
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
public class StatefulAlertingFunction extends
KeyedProcessFunction<String, SensorReading, String> {
// 1. Declare the physical state handle to remember the last temperature
private transient ValueState<Double> lastTemperatureState;
@Override
public void open(Configuration parameters) {
// This is the blueprint descriptor that hooks into the Flink State Backend
ValueStateDescriptor<Double> descriptor = new
ValueStateDescriptor<>("lastTemp", Double.class);
lastTemperatureState = getRuntimeContext().getState(descriptor);
}
@Override
public void processElement(SensorReading reading, Context ctx,
Collector<String> out) throws Exception {
// 2. Fetch the historical state for this specific sensor key
Double lastTemp = lastTemperatureState.value();
double currentTemp = reading.getTemperature();
if (lastTemp != null) {
// 3. Evaluate the current event against historical context
double delta = currentTemp - lastTemp;
if (delta > 10.0) {
out.collect("ALERT: Sensor " + reading.getId() + " jumped by " + delta + "°C!");
}
}
// 4. Update the state memory with the current temperature for the next event
lastTemperatureState.update(currentTemp);
}
}
State Management at Scale
When the stateful job runs in production, Flink distributes state across the cluster using keyBy(), co-locating each key’s state with the task instance responsible for processing it.
Distributed Snapshotting and Fault Tolerance
On every checkpoint barrier, Flink asynchronously snapshots each operator’s local state to durable storage (typically S3, HDFS, or a similar distributed filesystem) without halting the pipeline.
If a TaskManager fails, Flink spins up a replacement and restores the exact state for the affected keys from the last successful checkpoint, with exactly-once semantics, so no duplicate alerts or dropped metrics result from the failure.
State Modeling: Best Practices and Pitfalls
Things to Do
- Always set a State TTL. Streaming keys can arrive indefinitely (new sensor IDs, new user sessions). Without an expiration policy, state grows unbounded until you hit memory or disk exhaustion.
- Choose your partition key carefully. State is scoped per key under keyBy(). A skewed key, say 90% of traffic tied to one customer ID, overloads a single operator instance while others idle. Consider key salting if skew is unavoidable.
- Match the state backend to your workload. Use HashMapStateBackend when state fits comfortably in memory, and you need sub-millisecond access. Switch to EmbeddedRocksDBStateBackend once state size approaches or exceeds available cluster RAM; RocksDB spills to local disk and scales to terabytes of state per task.
- Use incremental checkpointing with RocksDB for large state sizes. Full checkpoints become expensive as state grows; incremental checkpoints only persist the delta since the last snapshot.
Things to Avoid / Trade-Offs
- Don’t use raw Java collections inside ValueState. A plain ArrayList or HashMap stored as a single state value forces full deserialization on every access. Use Flink’s native ListState or MapState instead; they support partial, incremental access.
- Don’t ignore serialization cost. Every read and write to a stateful operator serializes and deserializes data. This is a real, measurable CPU cost compared to stateless execution, so budget for it in capacity planning, especially with RocksDB where (de)serialization happens on every access, not just at checkpoint time.
- Don’t conflate checkpointing with savepoints. Checkpoints are automatic, lightweight, and used for failure recovery. Savepoints are manually triggered, versioned, and used for planned upgrades or job migrations. Treating them interchangeably leads to broken upgrade paths.
Wrapping Up
Stateless and stateful processing aren’t competing choices; most production topologies combine both in a layered design:
- Use stateless processing at the edge. Place filters, format converters, and validators directly behind your message broker (Kafka, Pulsar, Kinesis) to keep ingestion lean and fast.
- Reserve stateful processing for your core analytics layer: aggregations, windowed joins, alerting, and any logic where business context across events is a hard requirement.
![]()





AUTHOR
Apache Flink
Anil Kushwaha, Technology Head at Ksolves, is an expert in Big Data. With over 11 years at Ksolves, he has been pivotal in driving innovative, high-volume data solutions with technologies like Nifi, Cassandra, Spark, Hadoop, etc. Passionate about advancing tech, he ensures smooth data warehousing for client success through tailored, cutting-edge strategies.
Share with