Stream Processing Windows Ordering and Watermarks
Purpose and Outcome
Stream processing turns unbounded event logs into continuously updated answers: counts per minute, fraud scores per card, late shipment alerts, or rolling error rates. The hard part is that event streams are not naturally ordered by the time the business event happened. A mobile app can buffer events offline, a broker partition can lag, and a producer clock can be skewed. Windows, ordering rules, and watermarks are the machinery that lets a system answer time-based questions without waiting forever.
After this lesson you should be able to design a windowed stream processor, explain why event time differs from processing time, choose tumbling, hopping, sliding, or session windows, define a watermark policy, and reason about late events. This topic connects messaging and streams to storage, latency, correctness, and user-visible behavior. The question is how to produce a result that remains meaningful when input arrives out of order.
Mechanism: Event Time, Processing Time, and Window State
A stream record has three relevant times. Event time is when the fact occurred, such as a payment authorization at 12:01:08. Ingestion time is when the platform accepted it, perhaps at 12:01:11. Processing time is when a worker evaluates it, perhaps at 12:01:20. Windowing by processing time is simple and low latency, but it answers a different question: what did this worker see during its local interval? Windowing by event time answers the business question: what happened during the interval represented in the data?
A window assigns each event to one or more finite buckets. A tumbling window is fixed and non-overlapping, for example each minute. A hopping window has a fixed size and advances by a smaller step, such as a five-minute count emitted every minute. A sliding window is often defined around each event or query point and includes records within the last duration. A session window groups records separated by no more than a configured inactivity gap, which is useful for user visits or device activity bursts.
Internally, a stream processor maintains keyed state. For a query such as count orders per merchant per minute, the key is merchant and the window boundary is part of the state address. An event with merchant m7 and event time 12:01:08 updates state for key m7, window [12:01:00, 12:02:00). The processor may run many parallel tasks, so records are usually partitioned by key to route all updates for a key to the same logical owner. The state store keeps partial aggregates, timers, and sometimes a changelog so another worker can restore state after failure.
A watermark is the processor’s estimate of event-time progress. A watermark of 12:02:00 means the system believes it has probably seen all events with event time earlier than 12:02:00, subject to the configured lateness policy. Watermarks do not sort the stream globally. They decide when event-time timers fire, when a window can emit a final result, and when old state can be discarded. In distributed systems, each partition or input shard may produce its own watermark; the operator’s effective watermark is commonly the minimum across active inputs because one slow partition can still contain older events.
API and Configuration Anatomy
A windowed stream job usually contains five explicit choices. First, it extracts an event timestamp from each record. Second, it assigns records to keys. Third, it chooses a window assigner: tumbling, hopping, sliding, or session. Fourth, it defines watermark generation, often as a bounded out-of-orderness rule such as maximum observed event time minus two minutes. Fifth, it defines late-event behavior: drop, route to a side output, update previous results, or keep state open for an allowed lateness interval.
In pseudocode, the structure is: read from topic, parse event, assign timestamp, key by entity, apply window, aggregate, emit on watermark. Frameworks may call these timestamp assigners, window assigners, triggers, allowed lateness, grace periods, suppress operators, or punctuators. The vocabulary differs, but the underlying design is the same: state is updated by records, timers are advanced by watermarks, and finalized windows eventually become immutable enough to publish downstream.
Worked Example 1: Tumbling Counts
Suppose an API gateway publishes request events with service, status code, and event timestamp. You need requests per service per minute for dashboards. A tumbling event-time window is a good fit because each request belongs to exactly one minute, and the dashboard can tolerate updates arriving a little late. If service checkout has events at 10:00:02, 10:00:20, and 10:00:59, all three update the [10:00, 10:01) bucket. An event at 10:01:00 belongs to the next bucket because the end boundary is exclusive.
from collections import defaultdict
events = [
{'id': 'a', 'event_minute': 0, 'arrival': 0},
{'id': 'b', 'event_minute': 0, 'arrival': 1},
{'id': 'c', 'event_minute': 1, 'arrival': 2},
{'id': 'late', 'event_minute': 0, 'arrival': 4},
{'id': 'd', 'event_minute': 2, 'arrival': 5},
]
allowed_lateness = 1
max_event_minute = -1
counts = defaultdict(int)
emitted = []
for event in sorted(events, key=lambda item: item['arrival']):
max_event_minute = max(max_event_minute, event['event_minute'])
watermark = max_event_minute - allowed_lateness
minute = event['event_minute']
if minute >= watermark:
counts[minute] += 1
else:
emitted.append(('late_drop', event['id'], minute, watermark))
for window_minute in sorted(list(counts)):
if watermark > window_minute:
emitted.append(('final', window_minute, counts.pop(window_minute)))
print(emitted)
print(dict(counts))
This program models one-minute tumbling windows where event times are represented as minute numbers. The watermark lags behind the largest event time observed by one minute. A record is accepted when its event minute is at or ahead of the current watermark; otherwise it is too late for this simplified policy. When a stored window is older than the watermark, the example emits a final result and removes state. The expected output is a final count for minute 0 with three accepted events, while newer windows remain in state: [(‘final’, 0, 3)] and {1: 1, 2: 1}. Determinism comes from stable boundaries, event-time assignment, and watermark-driven cleanup rather than arrival order.
Worked Example 2: Hopping Fraud Windows
A payment system wants the number of declined authorizations per card over the last five minutes, refreshed every minute. A five-minute hopping window with a one-minute hop assigns each authorization to five windows. A decline at 12:04:30 contributes to windows ending at 12:05, 12:06, 12:07, 12:08, and 12:09, assuming aligned minute boundaries. This duplication is intentional: it gives downstream rules a moving view without computing every possible sliding interval.
event_minute = 4
window_size = 5
hop = 1
window_ends = []
for end_minute in range(hop, 10, hop):
start_minute = end_minute - window_size
if event_minute >= start_minute and end_minute > event_minute:
window_ends.append(end_minute)
print(window_ends)
The expected output is [5, 6, 7, 8, 9]. One event fans out into five updates because five aligned windows contain minute 4. That fan-out is the main design cost: one hot card or merchant can multiply writes and state updates. If a late decline arrives within the grace period, every affected hopping window must be corrected, not just the window matching the event’s minute. The useful behavior is that a burst affects several consecutive outputs, then ages out gradually as later windows no longer include the event.
Worked Example 3: Session Windows for User Activity
A learning platform tracks study sessions. A user opens lessons at 09:00, 09:03, 09:08, and 09:40. With a ten-minute inactivity gap, the first three events form one session from 09:00 through 09:08, and the 09:40 event starts a new session. Unlike tumbling windows, session windows are data driven. A late event at 09:12 can extend the first session to 09:12. A late event at 09:20 may even merge two sessions if the gap between neighboring events becomes small enough under the configured rule.
events = [0, 3, 8, 40]
gap = 10
sessions = []
for minute in events:
if not sessions or minute - sessions[-1][1] > gap:
sessions.append([minute, minute])
else:
sessions[-1][1] = minute
print(sessions)
The expected output is [[0, 8], [40, 40]]. The first three events stay in one session because each event is no more than ten minutes after the previous session end. The event at minute 40 starts a new session. Session windows fit user behavior but are more complex than fixed buckets. The processor must store start and end boundaries, not just a fixed bucket id, and it must support merging state when late events bridge sessions. Watermarks still matter because they determine when the processor stops waiting for events that could extend or merge older sessions.
Design Choices and Trade-offs
Choosing processing-time windows favors speed and operational simplicity. It is reasonable for worker health metrics or queue throughput, where the question is what the processor observed. Choosing event-time windows favors correctness for business facts, but requires timestamp extraction, watermark policy, late data handling, and more state.
A tight watermark, such as maximum observed event time minus five seconds, lowers result latency and state retention, but increases the chance of late drops or corrections. A loose watermark, such as minus fifteen minutes, improves completeness but delays final output and increases memory, disk, and changelog volume. Choose from measured lateness distribution: for example, the 99th percentile of mobile delay plus a small buffer if corrections are acceptable.
Window shape also matters. Tumbling windows are cheap. Hopping windows support rolling views but multiply updates. Sliding windows are expressive but can be expensive without specialized indexes or incremental aggregation. Session windows match human or device behavior but require mergeable state and careful cleanup. Name the query first, then select the window.
Failure Modes and Troubleshooting
Symptom: dashboard totals change after users thought a minute was final. The likely cause is late events being accepted after an initial emission. Check the configured grace period, the output mode, and whether downstream storage performs upserts keyed by window start and entity. Correct it by labeling results as preliminary until the final watermark, using a suppress/final-only operator, or making downstream consumers handle corrections explicitly.
Symptom: windows never close and state grows continuously. A stalled partition commonly holds back the minimum watermark. Inspect per-partition input lag, watermark metrics, and idle-source detection. If one source is idle but still considered active, enable an idleness policy or split the job so inactive partitions do not block unrelated keys. Also verify that producer timestamps are not far in the past.
Symptom: many events are marked late immediately after deployment. The timestamp extractor may be reading ingestion time, seconds as milliseconds, local time without timezone, or a nullable field defaulting to epoch. Sample raw records and compare extracted event time with broker append time and processing time. Fix the parser, reject malformed timestamps to a dead-letter stream, and backfill only after confirming the corrected extractor.
Symptom: one task is overloaded while others are idle. The cause is usually key skew, such as one merchant or tenant producing most events. Check records per key, state bytes per key, and task busy time. Corrections include salting hot keys for associative aggregates, pre-aggregating at the edge, isolating heavy tenants, or choosing a coarser result when exact per-key low-latency aggregation is not worth the cost.
Reliability, Performance, and Security Implications
Window state is reliability-critical data. If a worker fails before emitting a final result, another worker must restore partial aggregates from checkpoints or changelogs. Exactly-once output, when required, normally depends on coordinated state snapshots and transactional writes or idempotent upserts. At-least-once processing can duplicate updates, so aggregate sinks should use deterministic keys such as entity plus window start rather than append-only rows unless duplicates are acceptable.
Performance depends on active window count, key cardinality, event fan-out, and late-data retention. A one-minute tumbling count over 10,000 keys is very different from a one-hour hopping window every second over the same keys. Retention should exceed the largest window plus allowed lateness plus recovery margin, but keeping state forever hides bugs and increases restore time.
Security enters through timestamps and keys. A malicious or broken producer can send timestamps far in the future to advance watermarks and close legitimate windows, or far in the past to fill late-event paths. Validate timestamp ranges at ingestion, authenticate producers, and isolate tenants when one tenant’s disorder should not delay another tenant’s results.
Hands-on Lab: Simulate Watermark Decisions
Prerequisites: Python 3 and a terminal. No external packages are required.
- Create a scratch file named window_watermark_demo.py and paste the first Python program from this lesson.
- Run it with python3 window_watermark_demo.py.
- Confirm that the output contains a final tuple for minute 0 and that the count for minute 0 is finalized as 3 before cleanup from state.
- Change allowed_lateness from 1 to 2 and run it again. The watermark advances more slowly, so fewer windows finalize early and the late event is accepted longer.
- Add another event with event_minute 5 and a later arrival. Run the program and observe older windows becoming final as the watermark moves forward.
Verification: You have completed the lab when you can explain why each tuple appears in emitted and why some counts remain in state. The key check is not the exact list length after your modifications; it is whether every final emission corresponds to a window older than the current watermark.
Cleanup: Delete the scratch file when finished. No persistent resources were created.
Assessment Exercises
- A sensor sends events with event timestamps, but one region often buffers data for six minutes during network loss. Design a watermark and lateness policy for hourly energy usage totals. Explain what users see before and after finalization.
- You need a five-minute rolling error rate updated every ten seconds for alerting. Which window type would you choose, and what is the state cost compared with one-minute tumbling windows?
- A stream job’s watermark stops advancing while input records continue on most partitions. List the metrics you would inspect and describe the most likely cause.
- A late event arrives after a window has already been written to an analytics database. Compare dropping it, issuing a correction, and writing it to a side output for later reconciliation.
- For session windows, explain how a late event can merge two previously separate sessions and what this implies for the state store.
Summary
Windows make unbounded streams finite enough to aggregate. Ordering rules decide whether those aggregates represent arrival behavior or business event time. Watermarks are the event-time progress signal that closes windows, fires timers, and permits state cleanup. The main tension is latency versus completeness: faster watermarks produce quicker answers, while slower watermarks tolerate more disorder. A strong stream design states the timestamp source, key, window type, watermark policy, late-event behavior, output semantics, and retention plan together because each choice changes the correctness and cost of the others.
