Hotshard
Open the simulatorSimulator
Messaging & streaming

Stream Processing: Event-Time Windowing

Group events by when they happened, not when they showed up — and still be right when they show up late.

You want to count things over time: clicks per minute, sensor readings per hour, active users per five-minute window. Over a finished file this is easy — you already have every record, so you just group and count. A stream is different. The records never stop, and they do not arrive in the order they happened. A phone goes into a tunnel, buffers its events, and delivers them three minutes late. So the moment you ask "how many clicks happened between 10:00 and 10:01?" you hit the real question of stream processing: when do you stop waiting and commit to an answer? This page builds windowing and watermarks from that one problem.

Open the simulator →14 min

Counting by arrival time gives the wrong answer#

Say you are counting clicks per one-minute window. The obvious plan is to look at a clock on your server and bucket each click by the time it arrives. A click that lands between 10:00 and 10:01 goes in the 10:00 window. Simple, and it needs no extra information on the event at all.

The time your server reads off its own clock when it handles an event is the processing time. The time the event actually happened — stamped on it back at the source — is the event time. When everything is fast and in order these two are almost the same, and bucketing by arrival works fine.

Now one user is on a train that goes into a tunnel at 10:00:30. Their phone keeps recording clicks but cannot send them. At 10:02 the train comes out, and the phone flushes ninety seconds of buffered clicks all at once. Every one of those clicks happened before 10:01, but they all arrive during the 10:02 window. Bucket by arrival time and you have just credited the 10:02 minute with clicks that belong to 10:00 and 10:01. Every window is now wrong: the real busy minute looks quiet, and a quiet minute looks like a spike.

This is not a rare edge case. Mobile networks, retries, load balancers, and queues all reorder events. In a real stream a meaningful fraction of events arrive out of order by seconds, and some by minutes. If your counts are grouped by arrival time, they are grouped by the accidents of the network rather than by what actually happened.

The fix has a catchThe answer is to bucket by event time — the timestamp on the event, not your server's clock. Then the tunnel's clicks land in the 10:00 and 10:01 windows where they belong, no matter when they arrive. But this creates a new problem immediately: if a click for the 10:00 window can still show up at 10:02, you can never be sure the 10:00 window is finished. When is it safe to emit the count for 10:00 and move on? That question is the rest of this page.

Event time, and a watermark to tell you when to stop waiting#

TL;DRthe 30-second version
  • Group events by their event time — the timestamp of when they happened — not by when they arrive. This makes the counts correct even when data is late or out of order.
  • A window is a bucket of event time: for example every one-minute span. Each event joins the window that covers its event time and bumps that window's count.
  • The watermark is an estimate that says event time has now advanced past T, so no more events before T are expected. It is computed as the highest event time seen so far minus a fixed lag.
  • A window fires — emits its count — the moment the watermark passes the window's end. The lag is the grace period you grant late events before you commit.

Start with the window. A window is just a span of event time with a count attached. For one-minute tumbling windows, event time 10:00 to 10:01 is one window, 10:01 to 10:02 is the next, and so on. When an event arrives, you read its event time, find the window that covers it, and add one to that window's count. The tunnel's clicks, stamped 10:00:15 and 10:00:40, land in the 10:00 window even though they arrived at 10:02. That alone fixes correctness.

What it does not fix is knowing when a window is done. You are holding a running count for 10:00, and events for it can still trickle in. You need a signal that says: event time has moved far enough forward that the 10:00 window will get nothing more. That signal is the watermark.

The watermark is a single moving value: a claim that event time has certainly reached at least T, so no event with an event time below T should still be coming. The simplest and most common way to compute it is to take the highest event time you have seen so far and subtract a fixed amount, the watermark lag. If the newest event you have seen is stamped 10:03 and your lag is two minutes, your watermark is 10:01. You are saying: I have seen 10:03, so I am now confident everything up to 10:01 has arrived. The lag is your bet on how out of order the stream can be.

The watermark only ever moves forward. A late event does not drag it backwards, because that would un-finish windows you already closed. So the watermark is a monotonic estimate of progress through event time, always trailing the newest event by the lag you chose.

eventevent-time 10:00:40
read timestamp
assign to windowthe 10:00 bucket
bump
count += 1running total
then
advance watermarkmax seen − lag
if crossed
fire windowswatermark past end
How an event flows through

So the loop is: an event arrives, it joins the window covering its event time, the count goes up, the watermark advances, and every window the watermark has now passed fires and emits its count. The window for 10:00 fires the moment the watermark reaches 10:01 — which, with a two-minute lag, happens once you have seen an event stamped 10:03. Until then, 10:00 stays open and keeps accepting stragglers.

PredictYour watermark lag is two minutes. The newest event you have seen is stamped 10:05. A one-minute window covers 10:02 to 10:03. Has it fired yet?

Hint: Compute the watermark first: highest event time minus the lag.

Yes. The watermark is the highest event time seen (10:05) minus the lag (2 minutes), which is 10:03. The window's end is 10:03, and the watermark has reached 10:03, so it fired and emitted its count. A window fires the instant the watermark reaches its end — no sooner, because earlier the window might still gain a late event, and no later.

Three ways to cut the timeline#

The watermark decides when a window fires. The window type decides which events go together in the first place. There are three shapes, and picking the right one is most of what windowing design is.

Tumbling windows are fixed, back-to-back, and non-overlapping. Pick a size — say one minute — and event time is chopped into 10:00 to 10:01, 10:01 to 10:02, and so on. Every event belongs to exactly one window. This is what you want for reports that partition time cleanly: clicks per minute, revenue per hour, errors per day. The event at 10:00:40 lands in the 10:00 window and nowhere else.

Sliding windows have a fixed size too, but a new one starts every slide step, so they overlap. A five-minute window that slides every one minute gives you a fresh five-minute total every minute: 10:00 to 10:05, then 10:01 to 10:06, then 10:02 to 10:07. Because they overlap, a single event belongs to several windows at once — an event at 10:03 sits in all five of 09:59 to 10:04, 10:00 to 10:05, 10:01 to 10:06, 10:02 to 10:07, and 10:03 to 10:08 (size ÷ slide = 5 ÷ 1 = five live windows). This is the shape for smooth rolling metrics, like a five-minute moving average of request latency updated every minute.

Session windows have no fixed size at all. They grow with activity and close after a gap of inactivity. You set a gap — say thirty minutes — and every burst of events within thirty minutes of each other becomes one session; once thirty minutes pass with no event, the session closes. Two clicks a minute apart share a session; a click three hours later starts a new one. This is how you measure a user's actual visit rather than an arbitrary clock minute. Sessions are the one type whose boundaries depend on the data, so two nearby events can merge two half-built sessions into one.

WindowShapeAn event belongs toGood for
Tumblingfixed size, no overlapexactly one windowclean per-period reports: clicks/minute, revenue/hour
Slidingfixed size, overlappingseveral windows at oncerolling metrics: a 5-min average updated every minute
Sessiongrows with activity, gap-closedone session, which may mergereal activity spans: a user's visit, a burst of alerts

All three ride on the exact same event-time and watermark machinery. Only the rule for which window an event joins changes. That is why one engine can offer all three: assign the event to its window or windows, count, advance the watermark, fire whatever the watermark has passed.

A late event, saved and then dropped#

The whole design earns its keep on late events, so walk one through. Use one-minute tumbling windows and a watermark lag of two minutes. Events arrive stamped 10:00, 10:01, 10:03, and then, out of order, an event stamped 10:00 arrives after the 10:03 one.

  1. The 10:00 event arrives. It joins the 10:00 window; count is 1. Highest event time is 10:00, so the watermark is 10:00 minus 2 minutes, which floors at the start. Nothing fires.
  2. The 10:01 event arrives. It joins the 10:01 window; count is 1. The watermark creeps forward but has not passed 10:01 yet, so the 10:00 window stays open.
  3. The 10:03 event arrives. Highest event time is now 10:03, so the watermark advances to 10:01. That reaches the end of the 10:00 window, so 10:00 fires and emits a count of 1.
  4. The out-of-order 10:00 event arrives late. Its window, 10:00, already fired at the previous step. Whether it still counts depends on one setting: allowed lateness.

That last step is the crux. A window firing is not the same as a window closing. When the watermark passes a window's end, the window fires and emits a result — but you can keep it around a little longer to absorb stragglers. Allowed lateness is that grace period: an extra span past the window's end during which a late event still updates the window and re-emits a corrected count. Only when the watermark passes the end plus the allowed lateness is the window truly closed and purged, and only then does a late event get dropped to the side output, uncounted.

So if allowed lateness is one minute, the late 10:00 event that arrives while the watermark is at 10:01 still lands inside the grace period, updates the 10:00 window to a count of 2, and emits a correction. If allowed lateness is zero, the window closed the instant it fired, and the late event is dropped. Dropped does not mean lost silently — the event is routed to a side output, a stream of everything too late to count, so you can measure exactly how much data you are giving up.

Fires once, corrects, then closesKeep the three moments distinct. A window fires when the watermark reaches its end, emitting a first result. It may fire again — a correction — for each late event that lands within the allowed lateness. It closes when the watermark passes end plus allowed lateness, after which stragglers are dropped. The watermark lag buys you late events before the first fire; allowed lateness buys you corrections after it.

The one dial: how long to wait#

Everything reduces to a single trade, and it is worth stating plainly. The watermark lag decides how long a window waits before it fires. A larger lag catches more late events, because the watermark trails further behind and windows stay open longer. But a larger lag also delays every result, because the window cannot fire until the watermark reaches its end. You are choosing between completeness and latency, and you cannot have both.

  • Set the lag too small and the watermark races ahead. Windows fire quickly, but events that arrive even slightly out of order find their window already closed and get dropped. Your counts are fast and wrong.
  • Set the lag too large and you catch every straggler, but every result is held back by that full lag. A one-minute window with a ten-minute lag does not produce its count until ten minutes after the minute ended. Your counts are right and useless-for-now.
  • Allowed lateness softens the trade at the cost of extra work: fire early on a modest lag to get a fast provisional answer, then keep correcting as stragglers land within the grace period. The reader sees a number quickly and a right number eventually.

There is no universally correct lag, because it depends on how out of order your specific stream is. The honest approach is to measure: look at the distribution of how late your events actually arrive, and set the lag to cover, say, the ninety-ninth percentile. You will still drop the rare event that is later than that, which is why the side output exists — to tell you the true cost of the lag you picked.

What it costs to run#

The appeal of this whole approach is that the running state is small. You do not keep the events. You keep one number per open window — the running count, or a running sum, or whatever aggregate you are computing. When a window fires and closes, that state is freed.

Window typeState held at oncePer event
Tumblingone aggregate per window still inside the watermark's reachjoin one window, add one — constant work
Slidingone aggregate per overlapping window, so size ÷ slide of them at a timejoin size ÷ slide windows — a small constant
Sessionone aggregate per open session; a merge combines twojoin or extend one session, occasionally merge

The number of windows kept alive at any moment is bounded by the watermark. Once the watermark passes a window's end plus its allowed lateness, the window is purged and its memory reclaimed. So a bigger lag or a longer allowed lateness does cost more memory, because more windows stay open at once — another reason not to set them larger than your data demands.

Sliding windows cost the most, because one event fans out into every overlapping window. A five-minute window sliding every ten seconds keeps thirty windows alive, and each event updates all thirty. When that fan-out gets expensive, engines optimize it by keeping small partial aggregates per slide step and summing the ones a window covers, so an event is counted once rather than thirty times — but the mental model is still one aggregate per window.

Event time versus processing time, and batch versus stream

It is tempting to think processing-time windows are simply the wrong choice, but they have a place. Bucketing by arrival is cheaper — no watermark, no late-event handling, no held state waiting on stragglers — and for some questions arrival time is exactly what you want.

QuestionUseWhy
How many clicks happened in the 10:00 minute?event timethe answer is about when clicks happened; late data must count
How many requests is my server handling right now?processing timethe question is about the server's clock, so arrival time is the truth
Bill each advertiser for clicks in each hourevent time, exactmoney must reflect when the click happened, and be recomputable
Alert if error rate spikes in the last minuteprocessing timeyou want the freshest possible signal, and a few late errors do not change the alarm

The deeper point, made by Google's Dataflow work, is that batch and streaming are not really different systems. A batch job over a day's log file is just a stream where every event happens to have already arrived, so the watermark can jump straight to the end and every window fires at once. Streaming is the general case; batch is the special case where completeness is free because nothing is late. Once you see windowing and watermarks as the core, the old wall between batch and streaming stops being fundamental.

In production

Apache Flink is the reference implementation of this model. It supports event-time and processing-time semantics directly, assigns watermarks with pluggable strategies (a common one being bounded-out-of-orderness, which is exactly the highest-event-time-minus-lag rule this page uses), and offers tumbling, sliding, and session windows out of the box along with allowed lateness and a side output for dropped events. The vocabulary on this page is Flink's vocabulary.

The ideas were crystallized by Google's Dataflow model, described in the 2015 paper by Tyler Akidau and colleagues and popularized in his Streaming 101 and 102 articles. Dataflow separated four questions that earlier systems tangled together: what you are computing, where in event time the windows sit, when in processing time results fire (the watermarks and triggers), and how refinements to a fired result relate to it (the late-data corrections). That separation is why one framework can express batch and streaming as the same thing.

Kafka Streams and ksqlDB bring the same model to anyone already running Kafka: event-time timestamps carried on each record, tumbling, hopping (their name for sliding), and session windows, a configurable grace period that is exactly allowed lateness, and retention that bounds how long window state is kept. Spark Structured Streaming offers event-time windows with watermarks under the same names. The concepts transfer across all of them because they are the concepts, not one vendor's API.

Common mistakes
  • Windowing by processing time without meaning to. Many quick-start examples bucket by arrival because it needs no timestamps, and the wrong counts only show up under real out-of-order load. If correctness depends on when events happened, window by event time on purpose.
  • A watermark lag of zero. It looks clean and fires instantly, but any out-of-order event is dropped. Unless your source guarantees perfect ordering, a zero lag quietly discards data.
  • Forgetting the side output. Dropped events vanish unless you route them somewhere and measure them. Without that stream you have no idea whether your lag is dropping nothing or dropping a tenth of your data.
  • A stalled watermark from an idle source. The watermark is driven by the highest event time seen, so if one partition goes quiet, its watermark stops advancing and holds up every downstream window. Idle sources need an idleness timeout that lets the watermark move on.
  • Unbounded session or allowed-lateness state. A session with no gap, or an enormous allowed lateness, keeps window state alive indefinitely and leaks memory. Every open window must eventually close.
In an interview

When a design question involves counting or aggregating a stream — an ad-click counter, a metrics pipeline, a live leaderboard, a fraud detector — the interviewer is listening for whether you distinguish event time from processing time and whether you know how to decide a window is done. Lead with that distinction and the rest follows.

  • Open with event time versus processing time: bucket by when the event happened, because arrival order is corrupted by networks and retries. State that this is what makes late data count correctly.
  • Introduce the watermark as the answer to when a window fires: highest event time seen minus a lag, moving only forward. Name the lag as the completeness-versus-latency dial and say you would set it from the measured out-of-orderness of the stream.
  • Pick the window type deliberately: tumbling for clean per-period reports, sliding for rolling metrics, session for real activity spans. Say why the one you chose fits the metric.
  • Handle late data explicitly: allowed lateness to correct after firing, and a side output to measure what still gets dropped. Mention that a window firing and closing are different moments.
  • If exactness matters, like billing, mention the lambda-style pairing: a fast streaming count for the dashboard now, and a batch recompute over the durable log for the exact number later — the same model with the watermark jumped to the end.
Why not just wait a fixed amount of time before finalizing each window?

That is exactly what the watermark lag does, but expressed in event time rather than wall-clock time, which is what makes it correct. Waiting a fixed wall-clock time does not help if the stream itself slows down or a batch of old events arrives together; the watermark is tied to the event times you actually see, so it tracks real progress through the data.

What is the difference between the watermark lag and allowed lateness?

The lag delays when a window fires: it keeps the window open, gathering late events, until the watermark reaches its end. Allowed lateness is a grace period after firing: the window has already emitted a result, but it stays around a while longer to absorb stragglers and emit corrections before it finally closes.

Can the watermark ever go backwards when a very late event arrives?

No. It is defined to only move forward, because moving it back would re-open windows that have already fired and closed. A very late event does not lower the watermark; it either lands within an open window or its allowed lateness and gets counted, or it falls past a closed window and is dropped to the side output.

References
References

Feedback on this topic →