The full answer
Stateful processing means a streaming query remembers information across micro-batches, such as running counts, open sessions or the rows waiting for a join partner. In Structured Streaming, Spark keeps this state in a fault-tolerant state store on the executors, versioned and checkpointed with each micro-batch, and watermarks decide when old state can be removed.
Built-in stateful operations
- Aggregations:
groupBy(...).count(), running totals, and event-time windows (tumbling, sliding and session windows withsession_window). - Stream–stream joins: each side's rows are kept until a matching row can no longer arrive.
- Deduplication:
dropDuplicatesordropDuplicatesWithinWatermarkkeeps the keys already seen.
from pyspark.sql import functions as F
sessions = (events
.withWatermark("event_time", "30 minutes")
.groupBy(F.session_window("event_time", "10 minutes"), "user_id")
.agg(F.count("*").alias("events")))
Custom state For logic the built-in operators can't express, such as alerting when a device's reading stays high for 3 readings in a row, use arbitrary stateful processing:
transformWithState(Spark 4.0 and later;transformWithStateInPandasin Python) is the current API. It supports several state variables per key (value, list and map state), time-to-live for state, processing-time and event-time timers, and schema evolution of state.applyInPandasWithState(Python) andmapGroupsWithState/flatMapGroupsWithState(Scala, Java) are the older APIs, still supported.
Where the state lives
- The default state store keeps state in executor memory and writes versioned files to the checkpoint location.
- The RocksDB state store (
spark.sql.streaming.stateStore.providerClassset to the RocksDB provider) keeps state on local disk with a memory cache, which handles much larger state with less garbage collection; it is the usual choice for production. - Changelog checkpointing for RocksDB writes only the changes in each batch, which shortens checkpoint time.
Fault tolerance Each micro-batch's offsets and state version are written to the checkpoint. After a failure, the query restarts from the last committed batch, reloads the matching state version and reprocesses from the source, giving exactly-once results with replayable sources and idempotent sinks. The checkpoint must stay on reliable storage, and some changes to the query (such as changing grouping keys) are incompatible with existing state.
Keeping state under control
- Always use watermarks (or state time-to-live in
transformWithState) so state for finished keys is removed. - Watch state metrics in the query progress and the Structured Streaming UI: number of state rows and memory used.
- Partition state sensibly: the number of shuffle partitions is fixed for a query once it has a checkpoint.
The old DStream approach
DStreams used updateStateByKey and mapWithState. They are legacy; new stateful jobs use Structured Streaming.
Last updated 16 October 2026
Sources