Every streaming aggregation has to answer two questions: which events belong together, and when is a result final? This page explains event time, the main window types and watermarks, with Spark and Flink SQL examples.
Stream processors compute results over data that never ends: clicks, payments, sensor readings. To produce an answer such as "orders per minute", the unbounded stream must be cut into finite chunks called windows. Doing that correctly requires distinguishing two clocks:
A watermark is the processor's running estimate of event-time progress: a declaration that "no more events older than time W are expected". When the watermark passes the end of a window, the window can be finalised and emitted. Events that arrive after that point are late data.
Networks delay, mobile phones go offline, upstream batches are replayed, and partitions are consumed at different speeds. If you aggregate by processing time, a ten-minute network outage moves all its events into the wrong minute, and replaying yesterday's data gives different answers from running it live. Aggregating by event time gives correct, reproducible results — but then the system must decide how long to wait for stragglers. Watermarks are that decision made explicit: a tunable trade-off between latency (emit early) and completeness (wait for late events).
Each event is drawn twice: once at the time it happened and once at the time it arrived. Most arrive shortly after they occur, but e3 was created on a phone with no signal and arrives after e4 and e5. The gap between the two lines is called skew or lag, and it is neither constant nor bounded.
A common watermark strategy is bounded out-of-orderness: watermark = maximum event time seen so far minus a fixed delay, for example 30 seconds. When the watermark crosses 12:05, the window [12:00, 12:05) fires. The event stamped 12:03 that shows up at 12:09 is late. Engines offer three policies: drop it; route it to a side output for separate handling; or keep window state for an allowed lateness period and emit an updated result. Downstream consumers must then handle corrections, which is why sinks for updating streams are usually upserts keyed by window.
In parallel jobs each source partition has its own watermark and an operator uses the minimum across its inputs, so one idle or slow partition holds everything back. Flink therefore lets you mark sources idle (withIdleness).
Spark Structured Streaming expresses event-time windows with window() and the watermark with withWatermark(). This runs locally with the built-in rate source.
from pyspark.sql import SparkSession, functions as F
spark = SparkSession.builder.appName("windows").getOrCreate()
events = (spark.readStream.format("rate").option("rowsPerSecond", 20).load()
# simulate device timestamps up to 20 s in the past (out of order)
.withColumn("event_time",
F.col("timestamp") - F.expr("make_interval(0,0,0,0,0,0, rand()*20)"))
.withColumn("user", (F.col("value") % 5).cast("string")))
per_window = (events
.withWatermark("event_time", "30 seconds") # tolerate 30 s lateness
.groupBy(F.window("event_time", "1 minute", "30 seconds"), "user") # sliding
.count())
sessions = (events
.withWatermark("event_time", "30 seconds")
.groupBy(F.session_window("event_time", "10 seconds"), "user")
.count())
q = (per_window.writeStream.outputMode("append") # emit once watermark passes window end
.format("console").option("truncate", False).start())
q.awaitTermination(120)
With outputMode("append") each window is printed once, after the watermark passes its end; events older than the watermark are dropped silently. Switching to "update" prints running counts every micro-batch. The equivalent in Flink SQL:
CREATE TABLE clicks (
user_id STRING,
ts TIMESTAMP(3),
WATERMARK FOR ts AS ts - INTERVAL '30' SECOND
) WITH ('connector' = 'kafka', 'topic' = 'clicks', 'format' = 'json',
'properties.bootstrap.servers' = 'localhost:9092');
SELECT window_start, window_end, user_id, COUNT(*) AS clicks
FROM TABLE(TUMBLE(TABLE clicks, DESCRIPTOR(ts), INTERVAL '1' MINUTE))
GROUP BY window_start, window_end, user_id;
There is no universal right delay; it comes from your data. Start by measuring lag: for a day of events, compute processing time minus event time and look at the distribution. If the 99th percentile is 8 seconds and the 99.9th is 2 minutes, a 10-second delay drops about one event in a hundred, while a 2-minute delay drops one in a thousand at the cost of emitting every window two minutes later. Decide which matters more for the consumer. A fraud alert wants low latency and can tolerate a few missed events; a billing report wants completeness and can wait.
A common design uses both: a fast streaming result with a tight watermark for dashboards and alerts, and a periodic batch job over the same events, run hours later, that produces the authoritative numbers. This is sometimes called a lambda-style correction. With allowed lateness and upsert sinks, a single streaming job can often do both jobs.
Finally, publish the metrics: watermark lag behind wall-clock time, number of late events dropped, and state size. These three numbers tell you whether the delay is still right as traffic and devices change.
| Window | Overlap | Size | Typical use | State cost |
|---|---|---|---|---|
| Tumbling | No | Fixed | Per-minute metrics, billing periods | Low |
| Sliding / hopping | Yes (size / slide copies) | Fixed | Moving averages, alerting | Higher: each event in several windows |
| Session | No, per key | Data-driven | User sessions, device bursts | Variable; merges on late data |
| Global + trigger | N/A | Unbounded | Count-based or custom emission | Must be cleared manually |
| Engine | Watermark mechanism | Late data handling |
|---|---|---|
| Apache Flink | WatermarkStrategy in source or SQL DDL | Allowed lateness, side outputs |
| Spark Structured Streaming | withWatermark per stream; global min by default | Dropped after watermark; update mode re-emits |
| Apache Beam / Dataflow | Source-estimated watermarks, triggers | Allowed lateness, accumulating or discarding panes |
| Kafka Streams | Stream time + grace period | Grace period, suppression |