Streaming Aggregations with Watermarking
Write streaming aggregations with watermarking in Databricks.
Focus: write streaming aggregations with watermarking
Your streaming job counts clicks per user, but late events keep arriving after you've already written results — so your numbers are wrong, and you can't tell when it's safe to update. Without a strategy for handling late data, you either wait forever (and your dashboard is always behind) or you close the window too early and silently drop events. In Databricks, write streaming aggregations with watermarking solves this by telling the engine how long to wait for late data before finalizing state. By the end of this lesson, you'll understand the core concept, apply it in a hands-on exercise, and know how to connect this skill to your next step in the Databricks learning path.
The problem this lesson solves
Streaming data doesn't respect your schedule. A user clicks a button, but their event might land in Kafka seconds late — or minutes late due to network retries or client-side buffering. If you're computing a streaming aggregation like a count or sum over a tumbling window, you face a dilemma:
- If you emit results as soon as a window closes, you'll miss events that arrive just after that boundary.
- If you wait for every possible late event, you'll hold state forever and your results become stale — and your memory usage explodes.
Without a watermark, your pipeline either:
- Produces inaccurate results that silently change when late data finally arrives, or
- Blocks progress, delaying downstream consumers who need fresh results.
For example, imagine you're counting page views per 5-minute window to trigger a real-time promotion. An event that arrives 30 seconds late would be dropped if you closed the window immediately — costing you a sale. But if you wait 30 minutes for every event, your promotion logic never runs on time.
Watermarking gives you a controlled compromise: you define how late you're willing to wait, and the engine handles the rest — finalizing old windows, cleaning up state, and still allowing most late events to be included.
Why now? In any production streaming pipeline, late data is not an edge case — it's the norm. Master watermarking before you build anything more complex like stateful joins or sessionization.
Core concept / mental model
Think of a watermark as a clock that marks the point on the event-time timeline up to which the engine considers all data received. Anything earlier than the watermark is considered "too late" and is ignored. Events between the watermark and the current processing time are still welcome.
In Databricks (using Structured Streaming), you define a watermark with the withWatermark method on a streaming DataFrame. It takes a column that represents event time (e.g., the timestamp when the event actually happened) and a delay threshold (e.g., 10 minutes). The engine then:
- Tracks the maximum event time seen so far.
- Subtracts the delay threshold to get the current watermark.
- Keeps partial results for windows that haven't passed the watermark.
- Finalizes and outputs windows once the watermark moves past their end time.
Here's a mental picture:
Event time timeline
------------------>
maxEventTime = 12:00
watermark = 11:50 (12:00 - 10 min)
Window [11:45, 11:50) -> finalized (end <= watermark)
Window [11:50, 11:55) -> still open
A tumbling window is a fixed-size, non-overlapping time bucket (e.g., 5 minutes). The engine groups events by window, aggregates them, and emits a new row per window when the watermark moves past the window end.
Key mental model: The watermark isn't a wall that cuts off all late events — it's a moving boundary that ensures you are never more than
delaybehind the latest event time.
How it works step by step
Follow these steps to add watermarking to a streaming aggregation:
- Start with a streaming DataFrame — from a source (e.g.,
readStreamon Auto Loader, Kafka, or a Delta table). - Ensure you have an event-time column — if your source doesn't provide one, you may need to parse a timestamp field (e.g., from JSON) into a
TimestampTypecolumn. - Call
withWatermarkon the DataFrame, specifying the event-time column and the allowed lateness (e.g.,"10 minutes"). - Apply a windowed aggregation using
groupBy(window(event_time_col, "5 minutes"))and then an aggregate function likecountorsum. - Write the result to a sink using
writeStream(e.g.,outputMode("append")or"update") and start the query.
The engine automatically:
- Tracks the maximum event time across all partitions.
- Computes the watermark as
max(eventTime) - delay. - Outputs final results for windows whose end time is <= the watermark (in
appendmode). - Drops state for those windows, freeing memory.
Important: You must use the same event-time column in both withWatermark and the window function for watermarking to work. If you use a different column, the watermark won't be applied.
Hands-on walkthrough
Let's simulate a streaming source using rate or a simple socket. For local testing, you can create a tiny Kafka-like flow with readStream.format("rate"), but we'll use a more realistic example with a file source.
Example 1: Basic watermark with tumbling window
Assume you have JSON files landing in /tmp/events/, each containing user_id, event_time, and event_type. Set up a stream with a 10-minute watermark and a 5-minute tumbling window.
from pyspark.sql.functions import window, count
from pyspark.sql.types import StructType, StructField, StringType, TimestampType
schema = StructType([
StructField("user_id", StringType(), True),
StructField("event_time", TimestampType(), True),
StructField("event_type", StringType(), True)
])
# Streaming read from a directory (Auto Loader would be similar)
df = spark.readStream \
.schema(schema) \
.json("/tmp/events")
# Apply watermark and windowed aggregation
counts = df \
.withWatermark("event_time", "10 minutes") \
.groupBy(window("event_time", "5 minutes"), "user_id") \
.count()
query = counts \
.writeStream \
.outputMode("append") \
.format("memory") \
.queryName("clicks_per_user") \
.start()
query.awaitTermination(timeout=60)
When you run this in a notebook, you'll see results like this in the clicks_per_user table:
| window_start | window_end | user_id | count |
|---|---|---|---|
| 2024-01-01 10:00:00 | 2024-01-01 10:05:00 | u1 | 3 |
| 2024-01-01 10:05:00 | 2024-01-01 10:10:00 | u1 | 2 |
Only windows whose end_time is before the watermark appear — that's the guarantee of append mode.
Example 2: Using update mode for stateful results
Some use cases (like a running total) need update mode so you see latest aggregates even for unfinished windows.
# Same source, but use update mode to see partial results
query = counts \
.writeStream \
.outputMode("update") \
.format("console") \
.start()
query.awaitTermination(timeout=60)
In update mode, you'll see rows for every window, and they get updated as events arrive — but you still can't rely on them being final until the watermark passes the window end.
Pro tip: Use
appendmode for analytics that need final numbers (like a daily report) andupdatemode for live dashboards where partial numbers are acceptable.
Example 3: Watermark with event-time parsing
If your source stores event time as a string, parse it to a timestamp before applying the watermark.
from pyspark.sql.functions import to_timestamp, window, sum
# Assume 'ts' column is a string like "2024-01-01 10:00:00"
df = df.withColumn("event_time", to_timestamp("ts"))
aggregated = df \
.withWatermark("event_time", "5 minutes") \
.groupBy(window("event_time", "10 minutes")) \
.sum("amount")
Compare options / when to choose what
Watermarking changes how the engine handles state and output. Here's how it compares to alternatives:
| Approach | What it does | Best for | Drawbacks |
|---|---|---|---|
| No watermark (default) | Keeps all state forever, never finalizes windows | You have a bounded stream? Actually not recommended | Memory grows infinitely; results are never final |
| With watermark | Drops state after delay, allows late data within threshold | Most streaming aggregation jobs | You can still miss extremely late events |
| Drop duplicates | Uses watermark + dropDuplicates to dedupe |
Idempotent sinks, exactly-once semantics | Requires a watermark to clean state |
| Processing time window | Uses current_timestamp() as event time |
Testing, trivial use cases | Doesn't reflect true timing; not production-safe |
When to choose what
- Use watermark for any production aggregation where event-time accuracy matters — that's 99% of cases.
- Use
dropDuplicateswhen you're deduplicating events (e.g., click IDs) over a time window. - Avoid processing-time windows unless your data truly has no event-time column and you accept approximation.
Troubleshooting & edge cases
| Error / Symptom | Likely Cause | Fix |
|---|---|---|
java.lang.IllegalArgumentException: requirement failed |
Watermark column not in schema or wrong type | Ensure the column is of type TimestampType and exists in the DataFrame. |
Aggregations never output in append mode |
Watermark not moving; event times are not increasing | Debug by checking the max event time; maybe your source is stuck or event times are in the future. |
| Results are empty for what should be an active window | Window end hasn't passed watermark yet | Be patient — or use update mode to see partial output. |
| Memory grows endlessly | No watermark applied, or watermark set too high | Add a reasonable withWatermark (or lower the delay). |
| Late events are still dropped despite watermark | Delay threshold too small for actual lateness | Increase the delay; monitor event time distribution. |
Pro tip: Use
spark.conf.set("spark.sql.streaming.schemaInference", "true")for quick tests, but always define an explicit schema in production to avoid performance hits.
What you learned & what's next
You now understand the core pain of late data in streaming aggregations, the mental model of a watermark, and how to write a streaming aggregation with watermarking in Databricks. You can complete the hands-on exercise above — run it in a notebook with mock data and observe how output changes as you adjust the watermark delay.
Recall your learning objectives: you can explain the core idea behind write streaming aggregations with watermarking, and you've completed a practical exercise that demonstrates it. Next in the Databricks learning path, you'll build on this by exploring stateful streaming operations like joins with watermarking or sessionization — where the same principles extend to more complex pipelines.
Continue to the next lesson in the track to keep your momentum!
Practice recap
Open a Databricks notebook, create a mock stream from a file directory, and run the first example with a 10-minute watermark. Then change the watermark to 1 minute and observe how output changes — this will solidify your understanding of the trade-off between completeness and freshness.
Common mistakes
- Not applying
withWatermarkon the same column used in thewindowfunction — results in an error or ignored watermark. - Setting the watermark delay too small for your actual event lateness, causing legitimate data to be silently dropped.
- Using
appendmode for live dashboards and wondering why you don't see frequent updates —appendonly outputs final windows. - Forgetting to parse event-time strings into
TimestampType— the watermark expects a timestamp column. - Assuming a watermark guarantees exactly-once results — it's a best-effort compromise based on your delay threshold.
Variations
- Use
dropDuplicateswith watermarks for deduplication instead of a simple aggregation. - Use processing-time windows (via
current_timestamp()) when event-time data isn't available, but expect lower fidelity. - Apply watermarking in streaming joins (e.g., stream-stream joins) to limit state retention similarly.
Real-world use cases
- Clickstream analytics: counting user clicks per 5-minute window with a 10-minute watermark for late mobile events.
- IoT sensor data: computing average temperature per hour, tolerating 15-minute network delays from field devices.
- Financial transactions: summing payments per minute for fraud detection, allowing 30 seconds of late bank feeds.
Key takeaways
- Watermarks set a boundary on event-time lateness, allowing the engine to finalize windows and clean state.
- Use the same event-time column in both
withWatermarkandwindow; parse timestamps first if needed. - Choose
appendmode for final results andupdatemode for incremental live views. - Monitor your stream's event-time skew to tune the watermark delay appropriately.
- Watermarking is essential for production streaming aggregations to balance accuracy and memory usage.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.