How to Handle Out of Order Events Temporal
August 5, 2026 — In 2023, a fintech client — let's call them Quantia Health — came to us with a billing system that was retroactively charging patients the wrong copays. The data was correct. The order was wrong. A claim filed on Tuesday was being processed before a rate adjustment that happened on Monday, and the temporal system we built was treating the Tuesday event as truth because it arrived first—and then, when the Monday event showed up late, the entire pipeline panicked. My first instinct was to blame the queue. Turn out the queue was fine. Time was the problem. Here's how to handle out of order events temporal without losing your mind.
If you've ever asked how to manage out of order events in kafka, you're not alone. But the answer isn't a Kafka config. It's a mental model. In this guide, I'll walk you through the actual mechanics of event time vs. processing time, watermark strategies, and why your grandfather's slowly changing dimension table might be the most resilient pattern for modern streams. You'll learn how to handle out of order events temporal with concrete code, not just theory.
Why Your Stream Is a Lie
Most streaming systems assume the order of arrival equals the order of occurrence. That's a nice assumption. It's also wrong about ninety percent of the time in any system with retries, network partitions, or mobile clients.
Think about your phone. You're on a subway. The phone buffers events. You hit the station, internet comes back, and every event you generated in the last ten minutes fires in one burst — but the timestamps are an hour old. If your pipeline processes arrival order, you've just reordered reality.
This is the core problem of how does temporal work in streaming: the system needs to separate the "when it happened" timestamp from the "when we saw it" timestamp. Most people get this conceptually. The failure is in the implementation.
We tested five different approaches at SIVARO between 2022 and 2024. The one that consistently outperformed? Relying on event time and explicitly managing out-of-order windows, rather than pretending latency doesn't exist.
Watermarks Are Not Magic
Let's talk about Kafka, because that's where everyone starts. The question "how to manage out of order events in kafka" comes up every time a late event lands.
Kafka's log appends in arrival order. If you use ProcessingTime in your consumer, you'll process by arrival. That's fine for logs. It's worthless for temporal data. You need event time semantics, which means you need watermarks.
A watermark is a declarative boundary: "I'm confident events older than timestamp T have arrived." Everything before T is processed. Anything later is late.
python
# Kafka Streams DSL (simplified)
builder.<String, Event>stream("events")
.map((key, event) -> new KeyValue<>(event.customerId, event))
.windowedBy(TimeWindows.of(Duration.ofMinutes(5)))
.<code>suppress(Suppressed.untilWindowCloses(BufferConfig.unbounded()))
.to("aggregates");
But here's the part that gets people in trouble: you set a watermark of 5 minutes, and you think you're safe. You're not. Mobile devices, IoT sensors, and third-party data sources routinely push events 30, 60, even 120 minutes late. We had a client in the logistics space where trucks would go through tunnels and batch everything for 45 minutes.
The fix isn't a bigger watermark. It's a strategy. You need to handle the unavoidable late arrivals gracefully instead of pretending they don't exist.
The Lateness Pattern That Actually Works
At SIVARO, we've settled on a hybrid. We use Apache Flink for heavy lifting. The pattern is simple:
- Set a watermark for the 95th percentile of lateness.
- Allow the remaining 5% to be processed as "late data."
- Maintain a separate state store that merges corrections into aggregates.
- Emit a retraction or update when a late event changes a previous result.
This is how to handle out of order events temporal systems in the real world — accept imperfection, then correct it. Here's the Flink pattern:
java
DataStream<Event> events = env.addSource(kafkaSource)
.assignTimestampsAndWatermarks(
WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofMinutes(30))
.withTimestampAssigner((event, ts) -> event.occurredAt)
);
SingleOutputStreamOperator<Aggregation> results = events
.keyBy(event -> event.customerId)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.aggregate(new TotalAggregator())
.sideOutputLateData(lateTag);
DataStream<Aggregation> corrected = results.getSideOutput(lateTag)
.keyBy(agg -> agg.customerId)
.process(new MergingCorrector(stateStore));
The key insight? The aggregation window doesn't disappear. It still executes. The late output gets merged into the current state with a correction event. Your downstream systems need to handle idempotent updates — which is a different conversation, but it's the one that matters.
At first I thought this was a stream processing problem. Turns out it's a storage and retrieval problem. You're not just processing events; you're maintaining a view of history that can change. That's exactly what temporal tables were designed for.
Slowly Changing Dimensions: Your Old Friend
In 2024, I had a long conversation with Tim Mitchell about this exact challenge. He's written extensively about using temporal tables for slowly changing dimensions (Using Temporal Tables for Slowly Changing Dimensions), and his core point stuck with me: a database that natively tracks time is the most reliable place to reconcile out-of-order data.
SQL Server's system-versioned temporal tables make this trivial for operational workflows. When a late event arrives that updates a dimension, the temporal table automatically stores the old version in the history table. You don't need to write reconciliation logic or manually insert into audit logs. The database does it for you.
sql
CREATE TABLE dbo.DimCustomer
(
CustomerId INT PRIMARY KEY,
BillingTier NVARCHAR(50),
ValidFrom DATETIME2 GENERATED ALWAYS AS ROW START,
ValidTo DATETIME2 GENERATED ALWAYS AS ROW END,
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.DimCustomer_History));
Now, when a late rate adjustment arrives and updates the dimension, you don't lose the fact that the old rate was in effect at the time the claim was processed. The query engine will correctly attribute the claim to the old rate, because the temporal table tracks both versions.
This pattern, combined with a streaming watermark, is the most consistent approach we've tested for domains where "what was true at this instant" matters — insurance, billing, regulatory compliance. The Temporal Table Usage Scenarios - SQL Server documentation covers exactly this kind of point-in-time analysis, especially for slowly changing dimensions and correcting data retroactively.
Most people think temporal databases are an academic curiosity. They're wrong. For out-of-order events, the database is your safety net — it's the place where the true sequence gets reconstructed after the fact.
The Contrarian Take: Don't Event Sourcing Everything
Here's the contrarian position I've landed on after building production systems since 2018: event sourcing is overrated for out-of-order handling. Popular culture in the streaming world says "the event log is the source of truth." Yes, it is. But the event log arrival order is not the truth. The occurrence order is.
If you're doing event sourcing, every aggregate that receives a late event needs to replay its history with the corrected ordering. That's expensive desk work for every snapshot rebuild. The more I work with environments like Quantia's, the more I lean toward a simpler model: Temporal data modeling where you store facts with a valid time range)Skip the full event sourcing stack. Store event facts and their effective time ranges. On late arrival, perform a point-in-time correction. Don't rewind the world.
Here's an example of the schema pattern we use:
sql
CREATE TABLE fact_claim (
claim_id BIGINT PRIMARY KEY,
customer_id BIGINT,
amount DECIMAL(10,2),
mix_valid_from TIMESTAMP, -- business time
mix_valid_to TIMESTAMP, -- business time
mix_event_time TIMESTAMP, -- occurrence time
mix_ingest_time TIMESTAMP -- arrival time
);
If a late event changes the effective amount, we issue a correction. This is not a new event. It's an update to the validity range. The old row remains, but its mix_valid_to gets truncatedcontractual. The new row covers the rest of the period rank.
This style of handling out-of-order events is closer to Slowly Changing Dimensions (SCDs) than to classic CQRS event sourcing. And it scales better. You're not rewriting aggregates; you're patching the hops — and in the vast majority of cases, patching the hop is faster.
How Does Temporal Work in Streaming? The Practical Answer
People keep asking "how does temporal work in streaming" like there's a single answer. There isn't. But there is a best practice that emerged from the SIVARO pipelines we've built — including one for a national retailer's inventory sync that processes 200K events per second.
The answer is: three clocks, all distinct.
- Event time: when the thing happened (set by the producer).
- Ingest time: when you received it (set by the broker).
- Processing time: when you processed it (set by the consumer).
If you only use processing time for your windowing, your output is wrong whenever there's a network hiccup. If you only use event time, you deal with late arrivals — unavoidable. In practice, we always use event time for windows, ingest time for diagnostics, and processing time for dead-letter queues.
Let's show it in code:
python
from pyflink.datastream import StreamExecutionEnvironment
from pyflink.datastream.window import TumblingEventTimeWindows, Time
from pyflink.common.time import Duration
env = StreamExecutionEnvironment.get_execution_environment()
stream = env.from_source(source, watermark_strategy, "source")
stream.key_by(lambda e: e.customer_id) .window(TumblingEventTimeWindows.of(Time.minutes(5))) .allowed_lateness(Time.seconds(15)) .side_output_late(late_output_tag) .process(my_aggregator)
You'll notice the allowed_lateness(Time.seconds(15)). That's forgiveness. But here's the discipline: you can't allow infinite lateness. At some point, you need to mark a window closed and denormalize those key-values. The quieter workflows have a threshold. We set ours at 60 seconds for interactive dashboardshare and 24 hours for settlement tables. The rest are corrections.
The Event That Shows Up Twice (Because It Does)
Another hard lesson from the observation deck: out-of-order is not the same as duplicate. They often arrive together. Maybe exactly-once semantics in Kafka handles some of this, but I'll be honest — in practice, at 200K events/sec, we see duplicates every single day. Between retries, upstream replay, and human mistake, your system must be idempotent.
The de-dup logic we use:
- Binary-check with a key like
customerId + eventType + occurredAt. - Store the hash of each event in a Redis cache with a TTL matching your late-event threshold.
- On event arrival, query the cache. If hit → drop. If miss → process and store.
And regardless of how well with the semantics, you still need your downstream database to be naturally idempotent. That's where a constraining unique index on (customer_id, event_time, event_type) saves you. The database rejects the second insert. Your stream doesn't have to.
This part of the design — not how to manage out of order events in kafka but how to ensure correctness in the face of it — is what separates production-grade systems from demos.
When Relying on Temporal Tables Is Enough
I used to believe all temporal problems should be solved in the pipeline. Then we built a settlement engine for an insurance brokerage in 2025.This engine processes premium adjustments. The feed sends a batch of adjustments at 4:00 PM. Sometimes a correction arrives at 5:30 PM, retroactively changing the effective date.
Initially, we tried to feed the correction directly into the streaming aggregator.
It was a nightmare. Correcting state requires locking, merging, and re-emitting. The streaming processor swung from underutilized to 90% CPU whenever a correction batch landed.
Then we redesigned.
We parked the incoming events into a landing table, ran a stored procedure that used temporal tables to identify which rows were subject to change, and emitted the corrected output on a schedule—once every five minutes, not continuously.
The result? Same correctness, far less operational pain. The latest guidance on temporal data principles supports this point: temporal modeling isn't about streaming in real-time, it's about preserving the truth of time. Batch correction through a temporal table is a valid, sometimes superior, way to handle late-arriving data.
Designing for Eventual Temporal Consistency
Let's set the scene after all this warn-out. You want a system that handles out-of-order events without waking you up at 3 AM. Here's the recipe we use in every SIVARO client engagement:
- Discard nothing: even events that arrive a week late go into a raw replay bucket.
- Process with tolerance: watermark 30 minutes, allow lateness 15 seconds, side-output the rest.
- Store with revisions: use temporal tables or a valid-time model to support retroactive corrections.
- Queries are point-in-time: every dashboard and API endpoint needs a "business time as of" parameter.
Here's the query pattern that makes it work. If you're building for AS OF queries, temporal tables already supports it natively. But for generic SQL use:
sql
SELECT c.CustomerId, c.BillingTier, f.Amount
FROM dbo.DimCustomer c
FOR SYSTEM_TIME AS OF '2026-07-01 09:00:00' c
INNER JOIN dbo.fact_claim f ON f.customer_id = c.CustomerId
WHERE f.mix_valid_from <= '2026-07-01 09:00:00'
AND f.mix_valid_to > '2026-07-01 09:00:00';
The database reads the version of DimCustomer that was active at that instant, even if it has since been overwritten by a late-arriving event. No magic. Just design.
Handling Correctness in Mixed Order: A Real Example
Let me give you a concrete scenario from a manufacturer we worked with — let's call them Apex Manufacturing. They produce components for the automotive industry. Their IoT sensors emit events for machine readings. Sensors in the warehouse happen to be connected with some satellites that create latency in upload. A reading from 2 PM may arrive at 2:45 PM. The reactive team tried to process these in arrival order to maintain uptime dashboards analytics.
Riddle me this: health warnings depend on a cause reading followed by an effect reading. If the effect reading arrives at 2:45, timed 2:05, and the cause reading arrives at 2:50, timed 2:04, the effect processed before the cause. At that time of processing, the system thought the effect happened with unknown cause. The dashboard went red, flagged by the operator, created alarm fatigue.
The solution wasn't to raise the watermark. It was to change the detection logic.
We switched from "process each reading independently" to "maintain a short-lived state buffer for event sequences" — a mini state machine that waits for the cause event before emitting the aggregate result. Once the cause landmark passes through your watermark, any effect event without a cause is marked as orphan and set aside for manual review.
Here's a simplified version:
json
{
"sensor_id": "16-42",
"event_type": "TEMPERATURE_SPIKE",
"occurred_at": "2026-07-21T14:02:04Z",
"cause_event_id": "16-42-c98",
"cause_occurred_at": "2026-07-21T14:01:59Z"
}
Because we wait for the cause event before processing the effect, the event time order is respected regardless of the arrival jitter. This is the more robust interpretation of how to handle out of order events temporal — you're not just deduplicating; you're preserving causal ordering through a stateful transform.
The Hidden Cost of Lateness: State Size
There's a trade-off to everything. A larger watermark means more state. More state means higher memory usage arenas. At 200K events/sec, keeping 30 minutes of state for every pair can cost several gigabytes. During our inventory sync project, we had to tune Flink's RocksDB state backend to handle up to 40 GB of event sequences. The operations budget went up by two hundred thousand dollars a year. Was it worth it? For correctness, yesknow if you're in the 99th percentile vs the 50th, your state budget varies wildly.
Before you implement a huge watermark, profile your own lateness distribution. We did that for Quantia Health by dumping ingestion timestamps. The 95th percentile was 11 minutes. The 99th was 2 hours, 40 minutes. We chose a 30-minute watermark for the primary windowand a 3-hour side bucket for corrections. This saved us from paying for the 99th percentile across the board.
FAQ: Out of Order Events in Temporal Systems
What is the difference between event time and processing time?
Event time is the time the event actually occurred, as recorded by the producer. Processing time is when your pipeline consumes it. They diverge due to network latency, retries, and buffering. For temporal correctness, always window on event time.
How to manage out of order events in Kafka specifically?
Set your consumer to use event time. Use TimestampAssigner for Kafka's consumer records. Use WatermarkStrategy.forBoundedOutOfOrderness with a delay that matches your 95th percentile lateness. And use a separate topic or side output for events that exceed the lateness threshold.
How does temporal work in streaming for real-time dashboards?
For dashboards, you typically want processing time for display freshnessresident, but event time for accuracy. Best practice: display both. One line shows the live view; another shows the corrected view after late events arrive. That way you don't mislead operators.
Can temporal tables alone handle out-of-order events?
Yes, if you accept eventual consistency. Store the event as a new row, and let the temporal engine handle revisions. The tables will correctly respond to AS OF queries after a late event updates history. It's a strong pattern for analytical workloads.
What happens if the late event passes the watermark?
It's either dropped or sent to a side output. We recommend sending to a side output for later reconciliation. You may find that the five percent of late events cluster around specific vendors or channels — and you can solve their problems directly, not just globally.
Should I use allowed lateness or not?
Use it sparingly. Allowed lateness keeps windows alive, but it also delays results. We prefer a tight window with corrections afterward over an all-inclusive window that never closes. "Done" beats "perfect" when you have a correctness layer.
How do duplicates interact with out-of-order events?
They amplify the problem. Always make your processing idempotent. Use unique keys and cache key checks. A late duplicate is invisible if your downstream store is idempotent. It's a mandatory design pattern for temporal data.
What if I don't have a streaming platform?
You can still handle out of order events. Use temporal tables in a modern database and process late events in batch. You lose real-time accuracy, but you gain correctness. For most business applications, batch with temporal tables beats real-time with chaos.
The Bottom Line
Here's the truth no one wants to hear in the hot summer of 2026: distills down to being honest about uncertainty. You can't control when events arrive. You can control when you act on them and how you correct yourself.
We've built dozens of systems at SIVARO. The ones that succeed aren't the ones with the perfect watermark. They're the right combination of temporal patience, stateful buffering, and a database that can remember more than one version of the truth. The enterprise-level answer to how to handle out of order events temporal is the same for a handful of events as it is for a hundred thousand per second: don't confuse arrival order with event order, and build a correction path for every decision you make.
Read the literature on slowly changing dimensions and temporal databases — there's a deep vein of thought there that predates Kafka by decades (Slowly Changing Dimensions and Temporal Databases). The data warehouse crowd had this problem solved before streaming was mainstream. We're just re-learning it with lower latency. Take the best of both worlds: the immediacy of a stream and the patience of a history table. That's how you manage out of order events in kafka as a temporal system. That's how you win.
Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.