What you will be able to do
- Declare a watermark with withWatermark and predict when state for a window or key is dropped
- Choose a watermark duration and a multi-stream watermark policy, and know which stateful operations require a watermark
- Pick append, update or complete output mode for a stateful query, given its latency needs, how downstream readers use it, and the sink
Key concept
Watermark — A watermark is an event-time column plus a lateness threshold. The engine uses it to decide when a piece of state (a window or a join key) is final. Once the threshold passes, that state can be emitted and dropped, so state stays bounded instead of growing forever.
1.Why stateful queries need a watermark
A stateless streaming query only has to remember which source rows it has already processed. A stateful query also keeps intermediate results across micro-batches, so each batch can update results incrementally instead of recomputing everything. Typical state entities are a time-window aggregation and the unique keys in a stream-stream join. If nothing removes state, it keeps growing, and that leads to memory errors and rising latency. The watermark is what removes it.
You declare a watermark on a streaming DataFrame by naming a timestamp column and a lateness threshold. As data arrives, the state manager tracks the latest timestamp it has seen in that column and processes records that fall within the threshold. The guarantee only goes one way. Records inside the threshold are always processed. Records outside it might still be processed, but that is not guaranteed, so never build logic that relies on late rows being dropped.
from pyspark.sql.functions import window
(df
.withWatermark("event_time", "10 minutes")
.groupBy(
window("event_time", "5 minutes"),
"id")
.count()
)In groupBy() and window(), refer to the event-time column by name (a string or col()) so the event-time marker carries through to the aggregation.
Checkpoint 1 of 5· Check yourself
In the sample above, when is the state for one id's 5-minute window kept until?
The watermark is measured against the latest event time the engine has seen, not against the wall clock. A window's state is kept until its end is older than that maximum minus the 10-minute threshold.
“State information is maintained for each count until the end of the window is 10 minutes older than the latest observed event_time.”Source: docs.databricks.com
Sources1
2.Choosing thresholds: duration, joins, multiple streams and deduplication
Watermark length trades latency against tolerance for late data. A short watermark keeps less state and writes results sooner, but it tolerates very little late data. A long watermark tolerates more late data, but the query holds more state and waits longer before it writes results.
Stream-stream joins add some rules. For inner joins, Databricks recommends a watermark on each side so old records can be discarded. Without one, every key from both sides is joined on each trigger. For outer joins a watermark is mandatory. Unmatched rows come out with nulls, but only after the lateness threshold passes, because joins only support append mode. Each input can have its own threshold:
(input_stream1.withWatermark("eventTime1", "1 hour")
.join(
input_stream2.withWatermark("eventTime2", "2 hours"),
joinCondition)
)The engine tracks the maximum event time for each input separately, works out each input's watermark, and then picks one global watermark. By default it uses the minimum, so the query moves at the pace of the slowest stream. If one stream stalls, for example because of an upstream failure, its data is not wrongly marked as late. Setting spark.sql.streaming.multipleWatermarkPolicy to max uses the fastest stream's watermark instead. That lowers latency but drops data from the slower streams.
| spark.sql.streaming.multipleWatermarkPolicy | Global watermark follows | Consequence |
|---|---|---|
| min (default) | The slowest input stream | Safe: a lagging stream's data is not marked late, but output can be delayed |
| max | The fastest input stream | Lower latency, but data from the slowest streams is dropped. Use with caution |
Deduplication is stateful as well. distinct() keeps every unique record in state, so without a watermark that state grows forever. Structured Streaming guarantees exactly-once processing, but it does not remove duplicates that already exist in the source. To deduplicate on a key such as a GUID, even when duplicates have different event or arrival times, use dropDuplicatesWithinWatermark (Databricks Runtime 13.3 LTS and above). This method requires a watermark. To be sure every duplicate is dropped, set the threshold larger than the maximum time gap between duplicate events.
Checkpoint 2 of 5· Fill the gap
This query deduplicates on guid within a 10-hour window based on eventTime. Which method fills the blank?
(streamingDf
. ? ("eventTime", "10 hours")
.dropDuplicatesWithinWatermark(["guid"])
)dropDuplicatesWithinWatermark can only be used after a watermark is declared with withWatermark on the event-time column.
Source: docs.databricks.comSources1
3.Output modes: what a stateful query emits each trigger
Output mode controls which rows a query emits on each trigger. It matters only for stateful queries with aggregations. For stateless queries all modes behave the same. Joins support only append. Output mode has no effect on deduplication, and it does not change how mapGroupsWithState or flatMapGroupsWithState emit rows. If you set no mode, the query runs in append mode.
| Mode | Rows emitted per trigger | Latency and cost | Delta Lake sink |
|---|---|---|---|
| Append (default) | Only rows that won't change in future triggers. The watermark decides when that is | Delayed by at least the watermark delay. Each record is written once | Supported |
| Update | Every row that changed during the trigger, even if it may change again later | Freshest results. One write per trigger per aggregate value | Not supported. For similar behavior, see Merge in streaming |
| Complete | Every row the operator has ever produced (streaming aggregations only) | Can perform poorly as data scales | Supported |
After that first batch, state holds [2pm, 3pm] at $25 and [3pm, 4pm] at $30. Update mode emits both rows because both changed. Complete mode emits everything. Then $20 at 3:20pm arrives and the watermark moves to 3:05pm. That is past the end of [2pm, 3pm], so append mode now emits that window, which can no longer change. Update mode emits only [3pm, 4pm], whose value went from $30 to $50. Complete mode again emits every row.
Checkpoint 3 of 5· Put it in order
Put the events of the revenue example in order, ending with the first row append mode writes.
- 1.The watermark advances to 3:05pm
- 2.The watermark becomes 2:55pm and append mode emits nothing
- 3.The first micro-batch brings $15 at 2:40pm, $10 at 2:30pm and $30 at 3:10pm
- 4.Append mode emits the [2pm, 3pm] window
- 5.A $20 record at 3:20pm arrives
Each batch moves the watermark to the maximum event time minus 15 minutes. Append mode emits a window only once the watermark is past the window's end.
“The watermark updates to 3:05pm because the engine subtracts 15 minutes from 3:20pm.”Source: docs.databricks.com
Choose the mode based on how downstream consumers use the data. If a consumer takes one action per write, such as a notification service, use append so each record is written only once. If consumers need the freshest values, such as a real-time dashboard or an ML model reading features, use update. Then check that the sink supports the mode. Kafka supports all three. Delta Lake, which backs every Unity Catalog managed table, supports append and complete but not update, so an update-mode write to a managed table fails.
# Append output mode (same as default behavior)
(df.writeStream
.outputMode("append")
.toTable("target_table")
)Checkpoint 4 of 5· Check yourself
A windowed aggregation must keep a Unity Catalog managed table as current as possible, with rows overwritten as values change. Which statement is correct?
Managed tables use Delta Lake, which supports only append and complete. The documented way to get update-like behavior is to merge in streaming.
“Delta Lake, which backs all Unity Catalog managed tables, supports append and complete modes but not update mode.”Source: docs.databricks.com
Checkpoint 5 of 5· Exam question
A team runs the following Structured Streaming query: ```python agg = (df.withWatermark("event_time", "15 minutes") .groupBy(window("event_time", "5 minutes")) .count()) agg.writeStream \ .outputMode("complete") \ .format("delta") \ .option("checkpointLocation", chk_path) \ .toTable("orders.window_counts") ``` After running for several weeks, the checkpoint's state store directory keeps growing and the cluster shows increasing memory pressure. What is the most likely cause?
Correct answer: A — Complete output mode rewrites the entire result table on every trigger and keeps all aggregation state indefinitely, so the 15-minute watermark never triggers state eviction under this output mode.
- A. In complete output mode Structured Streaming re-emits the full result table on every trigger, so the engine must retain every aggregation group it has ever produced rather than dropping groups once they fall behind the watermark. Watermarks only enable state eviction in append and update mode, which explains the unbounded growth here.
- B. A shorter watermark relative to the window size makes Spark discard late data sooner, not reopen closed windows; Structured Streaming never re-creates a window once its close time has passed the watermark. This describes a data-correctness risk (dropped late events), not the memory-growth symptom in the scenario.
- C. Delta table sinks do not disable state store compaction, and Structured Streaming manages state store file compaction and cleanup automatically inside the checkpoint location without requiring a manual `VACUUM` on that path. This option describes a mechanism that does not exist in the streaming state store lifecycle.
- D. Calling `window()` with a single duration argument creates a tumbling window equal to that duration with no separate slide interval, so it does not default to a 1-minute sliding window. This mischaracterizes the API's default behavior and is not the source of the growth described.
Sources2
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.Update mode works on any sink, including a Delta table.Why is that wrong?
Delta Lake, and so every Unity Catalog managed table, supports only append and complete. An update-mode write to a managed table fails. Use a merge pattern instead.
Covered in Output modes: what a stateful query emits each trigger
2.Setting multipleWatermarkPolicy to max is a free way to cut the latency of a multi-stream query.Why is that wrong?
With max, the global watermark follows the fastest stream, so late-looking data from slower streams is dropped. The default, min, is the safe choice.
Covered in Choosing thresholds: duration, joins, multiple streams and deduplication
3.Append mode emits an aggregate window as soon as the window's time range ends.Why is that wrong?
Append mode waits until the watermark (maximum event time minus the delay) passes the window's end, so output is delayed by at least the watermark delay.
Covered in Output modes: what a stateful query emits each trigger
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“Queries always process records that arrive within the threshold. Queries might still process records that arrive outside the threshold, but this isn't guaranteed.”
↩︎ Why stateful queries need a watermark“By default, Structured Streaming uses the minimum as the global watermark.”
↩︎ Choosing thresholds: duration, joins, multiple streams and deduplication“For outer joins, watermarking is mandatory.”
↩︎ Choosing thresholds: duration, joins, multiple streams and deduplication“Structured Streaming guarantees exactly-once processing but doesn't deduplicate records from data sources.”
↩︎ Choosing thresholds: duration, joins, multiple streams and deduplication“Watermarks control the threshold for when a query stops processing a state entity.”
↩︎ Key concept“However, this configuration drops data from the slowest streams.”
↩︎ Exam trap 2“State information is maintained for each count until the end of the window is 10 minutes older than the latest observed event_time.”
↩︎ Checkpoint - 2.
“By default, streaming queries run in append mode.”
↩︎ Output modes: what a stateful query emits each trigger“Update mode results in one write per trigger per aggregate value.”
↩︎ Output modes: what a stateful query emits each trigger“Delta Lake sinks support only append and complete modes, so the update output mode example fails against a managed table.”
↩︎ Exam trap 1“your records have at least a 1-hour delay before being emitted downstream”
↩︎ Exam trap 3“The streaming aggregation operator does not emit anything downstream.”
↩︎ Prediction“The watermark updates to 3:05pm because the engine subtracts 15 minutes from 3:20pm.”
↩︎ Checkpoint“Delta Lake, which backs all Unity Catalog managed tables, supports append and complete modes but not update mode.”
↩︎ Checkpoint