This episode dissects the most important time concept in Flink: event time versus processing time. You'll generate watermarks to handle late-arriving data, get to know the tumbling, sliding, and session window types, and configure window triggers, allowed lateness, and side outputs for late events.

Episode 4 taught you the basic transformations and touched on the window operator. Episode 5 raises your accuracy level: most streaming analytics depend on time, and Flink offers a far more sophisticated time model than just the machine's clock. Without this understanding, your reports could be wrong simply because some events arrived late.
Topics we'll dissect: the difference between event time and processing time, how to generate watermarks to handle lateness, the three main window types — tumbling, sliding, and session — plus triggers, allowed lateness, and side outputs for late events. After this episode, you'll be able to build accurate time-based analytics even when data arrives late or out of order.
Processing time is the time when the processing machine handles the event. Event time is the time recorded inside the data itself, usually a timestamp created at the source. For analytics that must reflect when the actual event happened — for example, what time a transaction was made — event time is the only correct answer.
event time (time inside the data) vs processing time (machine time while processing)For event time to work, every event is expected to carry a timestamp field:
{
"userId": "u-1001",
"action": "purchase",
"amount": 250000,
"eventTs": "2026-08-10T08:15:00+07:00"
}The code above uses eventTs as the event time. You'll point to this field when setting up the watermark strategy.
A watermark is a marker that tells Flink: all events with an event time before this point are considered arrived. The most common strategy is bounded out-of-orderness — assuming events can arrive up to a certain number of seconds late:
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import java.time.Duration;
WatermarkStrategy<Order> strategy =
WatermarkStrategy.<Order>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((event, timestamp) -> event.getEventTs());forBoundedOutOfOrderness sets the lateness bound, and withTimestampAssigner tells Flink which field is the event time. The larger this bound, the more accurate the results, but the longer windows wait before firing.
The strategy is attached when the source is read, so the whole pipeline understands event time ordering:
DataStream<Order> orders = env.fromSource(
kafkaSource,
strategy,
"kafka-orders");Without this step, event-time-based windowing won't work — Flink can't assess lateness without a clear baseline.
A tumbling window divides the stream into fixed-size windows that don't overlap. Every event belongs to exactly one window:
orders.keyBy(Order::getUserId)
.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.sum("amount");Sliding windows can overlap — a new window opens every slide step. Suitable for computing moving averages:
orders.keyBy(Order::getUserId)
.window(SlidingEventTimeWindows.of(Time.minutes(10), Time.minutes(5)))
.sum("amount");A session window groups nearby events and closes itself after no event arrives for a given gap. Ideal for analyzing user sessions:
orders.keyBy(Order::getUserId)
.window(EventTimeSessionWindows.withGap(Time.minutes(30)))
.sum("amount");The choice of window type determines the shape of the aggregation: tumbling for periodic counts, sliding for moving trends, and session for activity patterns.
A trigger is the logic that decides when window results are emitted. Flink's built-in default is adequate for most cases: the window fires when the watermark passes its end boundary.
Even with watermarks configured, very late events can still arrive. Configure how long a window still accepts late events, then route the rest to a side output:
OutputTag<Order> lateTag = new OutputTag<Order>("late-orders") {};
SingleOutputStreamOperator<Order> windowed =
orders.keyBy(Order::getUserId)
.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.allowedLateness(Time.minutes(2))
.sideOutputLateData(lateTag)
.sum("amount");
DataStream<Order> lateOrders = windowed.getSideOutput(lateTag);
lateOrders.print();.allowedLateness extends the window's lifetime two minutes after the watermark passes it, and .sideOutputLateData separates events that arrive after that so they don't pollute the main aggregation.
The rule of thumb is simple: the larger the allowed lateness, the more complete the results, but the longer windows wait. Simulate this behavior by running a job and observing the results:
./bin/flink run -d target/window-job.jar
./bin/flink list -aThe ./bin/flink run -d command submits the job in detached mode, and ./bin/flink list -a shows its status. From the web dashboard, you can see when each window fires and how many late events go into the side output.
Episode 5 gave you full control over time: distinguishing event time from processing time, generating watermarks with forBoundedOutOfOrderness, choosing the right window type, and configuring triggers, allowed lateness, and side outputs for late events. These skills are the foundation of almost all correct streaming analytics.
The key takeaways:
In the next episode, episode 6, we'll discuss state management and checkpointing — stateful operators and key/value state, ValueState, ListState, MapState, and AggregatingState, checkpointing configuration and its consistency guarantees, plus restart strategies and fault tolerance. This is the key to building reliable Flink jobs that don't lose data in the event of a failure.