Filter concepts by levelShowing all levels.

System Design · Event Ordering and Duplicate Handling

Event Ordering and Duplicate Handling

Why global ordering is expensive, why duplicates and out-of-order events are normal, and the signals that establish true order

  • Partition-level orderingstrict within a key, no guarantee across keys
  • Idempotent + order-tolerantboth are needed together, not either alone
  • Version checksclock-independent, discard anything not strictly newer

1.Why global ordering is expensive

Global ordering means every event in a system, across every partition, every producer, and every machine, can be placed in one single, agreed-upon sequence — event 1 happened before event 2, which happened before event 3, no exceptions, no ambiguity, regardless of which server produced which event. Guaranteeing this is expensive because it requires coordination: every producer has to agree, in real time, on its position in one global sequence, which typically means funneling all writes through a single point (eliminating the parallelism a distributed system exists to provide) or running an expensive consensus protocol on every single event. This is why most real distributed messaging systems (Kafka included) deliberately do not offer global ordering as a default guarantee — instead, they guarantee ordering only within a partition (or equivalently, within all events sharing the same key), which is dramatically cheaper because a single partition can be owned and sequenced by one machine independently of every other partition, with no cross-partition coordination needed at all. The trade-off is real: two events for the same key are strictly ordered, but two events for different keys have no defined relative order, even if one was produced well before the other in wall-clock time. Most real workloads do not actually need global ordering — they need ordering per-entity (all changes to order #42 must apply in the order they happened; the relative order of a change to order #42 versus order #99 usually does not matter) — and partition-level ordering delivers exactly that, at a small fraction of the cost.

Key features

  • Global ordering requires coordination across every producer and every partition, which either funnels writes through a single point (destroying parallelism) or requires an expensive consensus protocol on every event
  • Partition-level (or key-level) ordering is what most real messaging systems, including Kafka, actually guarantee by default — one partition is owned by one machine at a time and sequences its own events independently, with zero cross-partition coordination
  • Two events for the same key/partition are strictly ordered; two events for different keys/partitions have no defined relative order, even if one was genuinely produced first in wall-clock time
  • Most real workloads only need per-entity ordering (all updates to the same order, the same account, the same document must apply in sequence) — which partition-level ordering delivers directly, by keying events on that entity's ID
  • Choosing a partition key is therefore a real design decision with ordering consequences: keying events by order ID guarantees order-level ordering; keying by a coarser or unrelated field can silently lose the ordering guarantee an application actually depends on
  • A system that genuinely needs global ordering across everything (rare) pays for it with a single point of coordination and reduced throughput — the cost is not hypothetical, it is the direct trade for a stronger guarantee than most workloads require

Global ordering vs. partition-level ordering

Global ordering vs. partition-level ordering
AspectGlobal orderingPartition-level ordering
Coordination neededEvery producer/partition, on every eventNone across partitions — each partition sequences independently
ThroughputLimited by the single coordination pointScales with the number of partitions
What is guaranteedOne total order across the entire systemStrict order within the same key; no guarantee across keys
Typical default in real systemsRare, and usually opt-in at real costKafka, most log-based systems, by default
Global ordering vs. partition-level ordering, for the same event stream

Global ordering

  • Every event across every partition placed in one sequence
  • Requires a single coordination point or a consensus protocol per event
  • Throughput bounded by that coordination cost
  • Rarely what the application actually needs

Partition-level ordering

  • Events for the same key strictly ordered
  • No coordination needed across partitions
  • Throughput scales with partition count
  • Matches the common real need: per-entity ordering
  • Global ordering
    • Every event across every partition placed in one sequence
    • Requires a single coordination point or a consensus protocol per event
    • Throughput bounded by that coordination cost
    • Rarely what the application actually needs
  • Partition-level ordering
    • Events for the same key strictly ordered
    • No coordination needed across partitions
    • Throughput scales with partition count
    • Matches the common real need: per-entity ordering

Command

text
# Partitioning by entity key: order-level ordering
# without any global coordination
producer.send(topic="order-events", key=order_id,
               value=event)
# All events for order_id=42 land in the same
# partition and are strictly ordered relative to
# each other. Events for order_id=99 may land in a
# different partition and have no defined ordering
# relative to order_id=42's events -- which is fine,
# because nothing in the application logic ever
# needs to compare the relative order of two
# different orders.

Global ordering (one sequence across the entire system) requires expensive coordination and is rarely what a real workload needs. Partition-level (or key-level) ordering — strict order within the same key, no guarantee across different keys — is what most systems, including Kafka, actually guarantee by default, and it is enough for the common real requirement: ordering per entity. Choose the partition key to match the entity that actually needs ordering, not for load distribution alone.

2.Consumers must tolerate duplicates and out-of-order events

At-least-once delivery — the standard guarantee most real messaging systems provide, because it is far cheaper than exactly-once — means a consumer will occasionally see the same event more than once (a producer retries after a network blip that actually succeeded, a consumer crashes after processing but before acknowledging), and a consumer that is not written to expect this will apply an event's effect twice. Even within a single partition's strict ordering guarantee, a consumer can still encounter events effectively out of order relative to its own processing — a rebalance, a retry, or a slow consumer that falls behind and gets events replayed from an earlier offset can all present events to application logic in a sequence that does not match production order, even when the underlying log itself preserved it correctly. Because both of these are normal, expected behavior of a real distributed system rather than rare edge cases, consumer logic has to be written defensively from the start: idempotent processing (applying the same event twice produces the same result as applying it once) handles duplicates, and order-tolerant processing (using a version number, timestamp, or explicit ordering key rather than assuming "the order I received it in is the order it happened") handles the out-of-order case. A consumer that instead assumes "each event arrives exactly once, in the exact order it happened" is not describing a rare failure mode when that assumption breaks — it is describing the everyday operating condition of the delivery guarantee it is actually built on.

Key features

  • At-least-once delivery (the common default) guarantees no message is lost, at the cost of occasional duplicate delivery — this is a normal, expected outcome of the guarantee, not a rare bug
  • Idempotent processing means applying the same event twice produces the same end state as applying it once — the standard technique is tracking a unique event ID and skipping (or making side-effect-free) any event ID already processed
  • Out-of-order delivery can happen even within a single ordered partition, due to consumer rebalances, retries, or replays from an earlier offset — the log preserved order, but the consumer's actual processing sequence can still diverge from it
  • Order-tolerant processing means using an explicit signal — a version number, a monotonic sequence number, or a timestamp with defined tie-breaking — to decide whether an incoming event is newer or older than the current state, rather than assuming arrival order equals production order
  • A consumer that applies updates purely in arrival order, with no version check, can have a newer state silently overwritten by an older, late-arriving event — a genuinely damaging bug, not a cosmetic one
  • Idempotency and order-tolerance are independent concerns that often need to be handled together: an event can be both a duplicate AND out of order relative to other events, and a consumer needs both defenses, not just one

Two failure modes and the defense each one needs

Two failure modes and the defense each one needs
Failure modeWhy it happensDefense
Duplicate deliveryProducer retries after an ambiguous failure; consumer crashes after processing but before acknowledgingIdempotent processing — track processed event IDs, skip repeats
Out-of-order arrivalConsumer rebalance, retry, or replay from an earlier offsetOrder-tolerant processing — compare a version/timestamp, not arrival order
A duplicate and an out-of-order event, both handled safely
Producer
Queue
Consumer
State Store
  1. 1publish UpdateV3
  2. 2retry: publish UpdateV3 againambiguous failure — duplicate
  3. 3deliver UpdateV1
  4. 4deliver UpdateV3applied — version 3 > current
  5. 5deliver UpdateV3 (duplicate)skipped — already processed
  6. 6write only for genuinely new, in-order updates
  1. Producer → Queue: publish UpdateV3
  2. Producer → Queue: retry: publish UpdateV3 again (ambiguous failure — duplicate)
  3. Queue → Consumer: deliver UpdateV1
  4. Queue → Consumer: deliver UpdateV3 (applied — version 3 > current)
  5. Queue → Consumer: deliver UpdateV3 (duplicate) (skipped — already processed)
  6. Consumer → State Store: write only for genuinely new, in-order updates

Command

python
# A consumer defended against both duplicates and
# out-of-order arrival
def handle_event(event):
    if event.id in already_processed:      # duplicate defense
        return
    current = get_current_state(event.entity_id)
    if current and current.version >= event.version:
        already_processed.add(event.id)     # older/duplicate:
        return                              # skip, don't overwrite
    apply(event)
    already_processed.add(event.id)

At-least-once delivery guarantees no message is lost at the cost of occasional duplicates, and even ordered partitions can present events out of processing order due to rebalances or replays — both are normal, expected conditions, not rare edge cases. Idempotent processing (dedupe by a unique event ID, not by content) handles duplicates; order-tolerant processing (compare an explicit version or timestamp, not arrival order) handles out-of-order arrival. A consumer needs both defenses together, since an event can be affected by either or both at once.

3.Timestamps, sequence numbers and version checks

Once a consumer accepts that arrival order is not a reliable signal of true ordering (the prior concept), it needs an actual mechanism to determine which of two events is genuinely newer, and three mechanisms cover almost every case. An event timestamp records when an event happened, typically set by the producer at creation time, and lets a consumer compare "which of these two events happened first" directly — but timestamps from different machines are only as reliable as those machines' clocks, and clock drift (covered in the next section) can make two genuinely-ordered events appear to have times in the wrong order. A sequence number is a monotonically increasing counter, usually scoped to a specific entity or partition, that a producer increments on every event for that entity — unlike a timestamp, it has no dependency on wall-clock accuracy at all, since it is just a counter that only ever goes up, making it a stronger ordering signal wherever it is available. A version check (often called optimistic concurrency control) is the version-number variant already used for idempotency in the prior concept, applied specifically as an ordering gate: before applying an update, a consumer compares the incoming event's version against the current stored version, and only applies it if the incoming version is actually newer, which is what makes stale, late-arriving events safe to simply discard rather than needing to be correctly sequenced in the first place. In practice, sequence numbers or version checks are usually preferred over raw timestamps wherever an entity has one clear owner producing its events, precisely because they sidestep the clock-reliability problem timestamps carry.

Key features

  • Event timestamps let a consumer compare "which happened first" directly, but are only as trustworthy as the producing machines' clocks — clock drift between producers can make timestamps disagree with true event order
  • Sequence numbers are a monotonic counter (usually per-entity or per-partition) with no dependency on wall-clock time at all — a stronger signal than a timestamp wherever a single producer can maintain the counter
  • A version check (optimistic concurrency control) compares an incoming event's version against the currently stored version and only applies the update if it is strictly newer — this is the same version field used for idempotency, doing double duty as an ordering gate
  • Sequence numbers and version checks are usually preferred over raw timestamps specifically because they sidestep clock-reliability problems entirely — they never needed a clock to be correct in the first place
  • These three mechanisms are not mutually exclusive — a system can record a timestamp for human-readable auditing and diagnostics while using a sequence number or version for the actual ordering/conflict decision that code relies on
  • A version check does not require reconstructing the true global order of every event — it only needs a local, per-entity comparison ("is this newer than what I currently have"), which is exactly why it is cheap enough to apply on every single update

Three ordering mechanisms and what determines their reliability

Three ordering mechanisms and what determines their reliability
MechanismReliability depends onTypical use
Event timestampProducer clock accuracy and synchronizationHuman-readable ordering, audit trails, approximate ordering across sources
Sequence numberA single producer's ability to increment a counter correctlyStrict per-entity or per-partition ordering, independent of any clock
Version checkThe same counter/version, used as a comparison gate before applying an updateDiscarding stale/late-arriving updates without needing global order
Three signals, three different jobs

Event timestamp

Wall-clock time

human-readable, clock-dependent

Sequence number

Monotonic counter

no clock dependency at all

Version check

Compare before applying

discards stale updates without needing global order

  • Event timestamp
    • Wall-clock time — human-readable, clock-dependent
  • Sequence number
    • Monotonic counter — no clock dependency at all
  • Version check
    • Compare before applying — discards stale updates without needing global order

Command

python
# Three signals recorded on the same event, each
# serving a different purpose
event = {
    "entity_id": "order-42",
    "sequence": 7,                    # monotonic,
                                       # per-entity
    "version": 7,                     # same value
                                       # here, used
                                       # as an
                                       # optimistic-
                                       # concurrency
                                       # gate
    "produced_at": "2026-08-26T10:03:21.442Z",  # for
                                       # humans/audit
                                       # trails only
}

def apply_update(event, current_state):
    if event["version"] <= current_state.version:
        return  # stale or duplicate -- discard
    current_state.apply(event)
    current_state.version = event["version"]

Timestamps let you compare "which happened first" but are only as reliable as producer clocks; sequence numbers are a clock-independent monotonic counter, usually per-entity, which sidesteps that problem entirely; a version check compares an incoming event's version against the current stored version and discards anything not strictly newer, without needing to reconstruct global order at all. Prefer sequence numbers or version checks over raw timestamps wherever an entity has one clear owner producing its events, and keep any sequence counter scoped per-entity, not global.

Remember

  • Global ordering requires expensive coordination; partition-level ordering is what most systems actually guarantee and what most real per-entity ordering needs actually require.
  • At-least-once delivery makes duplicates and out-of-order arrival normal, expected conditions — a consumer needs both idempotent and order-tolerant processing, not just one.
  • Sequence numbers and version checks are clock-independent and usually preferred over raw timestamps for the actual ordering decision.

Pro tip

  • Choose a partition/sequence key that matches the entity actually needing ordering, not a field chosen only for even load distribution.
  • Deduplicate by a unique event ID assigned at production time, never by comparing event content — two distinct, legitimate events can look identical.

Next up

  • Distributed Time and Clocks
  • Wall-clock drift and process clocks
  • UTC, monotonic clocks, logical ordering

Sources

  • Apache Kafka Documentation — Semantics
  • AWS SQS Developer Guide — FIFO Queues
  • Checked 2026-08-26

Spotted a mistake on this page? Tell us.