Event vs processing time, tumbling and sliding windows, watermarks for late data, stateful operations, and exactly-once.
⏱ 13 min readTopics chapter readerLevel · Streaming
01 · Orientation
Start here
⏱ 5 min · Topic 1 of 12
In "Stream Processing" you learned the ideas: event time, windows, watermarks and state. This chapter shows the Spark code for each idea, and the rules Spark adds.
"Structured Streaming Fundamentals" came first. It explained micro-batches, output modes and the checkpoint. Here you use them.
The data is the shop stream again, on 2026-01-22. Spark reads the orders in the order they arrive, in micro-batches 0, 1 and 2. Order 113 is late: it happened at 10:04 and arrives at 10:07.
The shop stream, in arrival order
Batch
Order
Customer
Amount
Event time
0
110
2
30.00
10:02
0
111
5
9.00
10:04
0
112
3
20.00
10:06
1
113
1
12.00
10:04
1
114
4
7.50
10:08
2
150
2
18.00
10:09
2
151
1
8.00
10:10
The chapter has ten parts.
Event time and tumbling windows.
Sliding windows.
Session windows.
Watermarks and state.
Late rows.
Joining two streams.
Removing duplicates.
Your own state, with timeouts.
The state store and how to monitor it.
A recap with questions.
02 · Grouping by time
Event time & windows
⏱ 6 min · Topic 2 of 12
Event time is when something happened. It is a column in the data. Here it is order_ts, the time the customer placed the order.
A window is a block of event time, such as 10:00 to 10:05. The window starts at 10:00 and stops just before 10:05. A tumbling window block never overlaps the next one.
The function window() puts each row into its block. Look at a count and a total for each 5-minute window, with no watermark yet.
Orders and totals for each 5-minute windowworked example
Order 113 arrives in batch 1, but its event time is 10:04. So it goes into the 10:00 window, which grows from 2 to 3 orders.
The window column holds a start and an end. Here it is shown as text.
Update mode prints only the windows that changed. In batch 2, the 10:00 window did not change, so it is missing. Order 151 at exactly 10:10 starts a new window.
Spark must remember the count and total of every window across batches. This remembered data is called state. It is kept for you, and it is saved in the checkpoint.
Watch out
Without a watermark, Spark keeps every window forever, because a late row could still arrive. The state only grows. The watermark fixes this.
03 · Overlapping windows
Sliding windows
⏱ 5 min · Topic 3 of 12
A sliding window has a length and a slide. A 10-minute window with a 5-minute slide starts every 5 minutes. Each window is 10 minutes long, so windows overlap.
One row falls into two windows. Look at orders 110 to 114 with this query.
Order 110 alone would end at 10:05. Order 190 at 10:04 comes before that, so the session grows to 10:04 plus 3 minutes, which is 10:07.
Order 150 at 10:09 comes after 10:07, so it starts a second session.
A late row can fill the gap between two sessions. Then Spark joins them into one. For this reason, Spark can only call a session final when the watermark reaches its end.
05 · Letting state go
Watermarks and state
⏱ 5 min · Topic 5 of 12
A watermark is a cut-off in event time. It is the newest event time Spark has seen, minus a delay. A window that ends at or before the cut-off is final.
You set it with withWatermark(column, delay). We use a delay of 3 minutes, the same as in "Stream Processing". Look at the same window query, in append mode.
A watermark of 3 minutes, in append modeworked example
In batch 2 the cut-off is 10:05. The window 10:00 to 10:05 ends at 10:05, so it is final. Spark prints it once, with all 3 orders, and drops its state.
The window 10:05 to 10:10 ends at 10:10. It is final only when the cut-off reaches 10:10.
Follow these four rules.
Spark works out the watermark at the end of a batch, and uses it in the next batch. A batch never uses its own rows for its own cut-off.
Call withWatermark before the aggregation, on the same column that the window uses. Otherwise Spark does not use it to clear state.
The watermark never goes down. A late row cannot move it back.
Append mode prints a window once, after it is final. Update mode prints a window every time it changes, and uses the watermark to drop old state and too-late rows.
Watch out
The watermark moves only when new rows arrive. If the stream goes quiet, the last windows stay unwritten in append mode.
06 · Rows that come late
Late rows
⏱ 6 min · Topic 6 of 12
Two rows are late in the shop stream. Order 113 arrives in batch 1. Order 160 arrives in batch 3.
Order 160 is the very late order from "Stream Processing". It was placed at 10:03 and arrives alone, at offset 7, in a new batch. Look at what the watermark does to each late row.
Kept. 10:04 is after 10:03. It is added to the 10:00 window.
160
Dropped. The 10:00 window was written in batch 2 and is gone.
Order 113 is only 2 minutes behind the newest event time before batch 1, which is 10:06. Order 160 is 7 minutes behind 10:10. The 10:00 total stays 51.00.
Order 113 shares batch 1 with order 114 at 10:08. Even so, batch 1 still uses the old cut-off, 10:03.
Spark gives one promise. Rows that are no more than the delay behind the newest event time are kept. Spark does not promise to drop older rows.
An older row can still be counted if its window is still in state. Never write logic that depends on a row being dropped.
Pick the delay with care. A longer delay catches more late rows, but holds more state and writes append results later. A shorter delay does the opposite.
Interview question
What is a watermark, and what does it guarantee? A good answer: it is the newest event time minus a delay. Spark keeps data inside the delay, and it may drop older data.
07 · Two streams
Join two streams
⏱ 6 min · Topic 7 of 12
A stream-stream join matches rows from two streams. Say a second stream holds payments. Spark must keep rows in state while a match can still arrive.
You give Spark a time rule, so it knows when to stop waiting. Here a payment must come at most 5 minutes after the order. Both payments are made up.
Order 111 has no payment, so an inner join does not return it. A left outer join returns it with NULL, but only later.
Stream-stream joins write in append mode only.
With "left_outer", Spark writes order 111 with NULL only after the watermark passes 10:09, which is 10:04 plus 5 minutes. Until then, a payment could still come.
Here is what each join type needs.
Stream-stream joins
Join
Watermark
Time rule
What happens without them
Inner
Optional
Optional
It works, but state grows forever.
Left or right outer
Needed
Needed
Spark cannot know when to write the NULL row.
A query has one global watermark. With two streams, Spark uses the smaller value by default.
The setting spark.sql.streaming.multipleWatermarkPolicy controls this. Its default is min.
08 · Duplicates
Remove duplicates
⏱ 5 min · Topic 8 of 12
A source can send the same order twice, for example after a retry. You met this in "Delivery Guarantees & Ordering". In Spark, you remove duplicates with dropDuplicates.
Look at three rows. Order 110 arrives twice, with the same event time.
The second copy of 110 matches a key Spark remembers, so Spark drops it. Spark keeps the key in state.
The event time must be in the list of columns, so Spark can forget a key when the watermark passes it.
There are three ways to do it.
Three ways to remove duplicates
Code
State
Duplicate with a different event time
dropDuplicates(["order_id"])
Every id, forever
Caught
Watermark, then dropDuplicates(["order_id", "order_ts"])
Cleared as the watermark passes
Not caught
Watermark, then dropDuplicatesWithinWatermark(["order_id"])
Cleared as the watermark passes
Caught, if inside the delay
The third way needs Spark 3.5 or later. Use it when a retry can change the event time.
09 · Your own state
Your own state
⏱ 6 min · Topic 9 of 12
Sometimes a built-in aggregation cannot do the job. You then write a function that keeps its own state for each key. Spark calls it for every key that has new rows.
In PySpark 3.4 and later, the method is applyInPandasWithState. In Scala and Java, use flatMapGroupsWithState or mapGroupsWithState.
Look at one example. It counts how many orders of 10.00 or more each customer placed in a row. A smaller order sets the count back to 0.
Customer 2 has orders of 30.00 and 18.00, so the streak is 1 and then 2. Customer 1 has 12.00 and then 8.00, so the streak goes to 1 and then back to 0.
Each customer has one order per batch here, so the order of rows inside a batch does not matter.
The function reads the old streak with state.get and saves the new one with state.update. Spark stores it in the state store and in the checkpoint.
A timeout lets you act when a key goes quiet. With a processing-time timeout, you set timeoutConf to GroupStateTimeout.ProcessingTimeTimeout and call state.setTimeoutDuration. Later, Spark calls your function with no rows, and state.hasTimedOut is true.
Watch out
Spark never drops this state for you. Call state.remove when you are done with a key, for example when its timeout fires. Otherwise every key stays in state forever.
10 · Running it
State store and monitoring
⏱ 5 min · Topic 10 of 12
All state lives in the state store. By default, it keeps the state in memory on the executors, and saves copies in the checkpoint folder.
Large state can fill the memory. Spark 3.2 added the RocksDB provider, which keeps state on local disk. You turn it on with one setting, before the first run of a query.