Out-of-order events are events that reach your system in a different sequence from the one in which the source system created them. If your integration applies each event as it arrives, a late event can overwrite newer, correct data with older state. A cancelled subscription can come back to life. A shipped order can slide back to "packed". A credit limit can revert.
This is a different problem from duplicate delivery. Our guide to idempotent webhooks and API retries covers the "same event twice" case. This guide covers the "old event after new event" case, and the concurrent-writer and unknown-outcome cases that sit next to it.
It is written for the enterprise architect who owns the integration contract between systems. You will get a decision model, a proposed design pattern, an honest list of trade-offs, and a set of mandatory go/no-go controls. Code samples are illustrative. Check every vendor behavior against the vendor's current documentation before you build on it.
Key Takeaways
- Ordering and deduplication are separate controls. An event ID table stops duplicates. It does not stop a stale event from overwriting newer state.
- Do not infer order from timestamps. Stripe's documentation says its event timestamps are recorded in seconds, that distinct events can share a timestamp, and that you should not use them to determine event order.
- Put the ordering check inside the write. A read, then a compare, then a write in application code is a race. Use a target-side conditional mutation, or disclose the serialization limits you are relying on.
- Every event needs a recorded disposition: applied, superseded, parked, rejected, unknown or terminal failure. "Processed" is not a disposition.
- Acknowledge only after a durable commit. Dedup record, state change and scheduled downstream work must commit atomically, or be explicitly recoverable.
- Use go/no-go controls, not a score. A weighted total can hide one fatal gap. Every control in the checklist below must pass.
Why Out-of-Order Events Corrupt Good Records
Key takeaway: Retries, parallel consumers, replays and backfills make reordering normal behavior. It is not an edge case.
Most event sources promise delivery, not sequence. Stripe's webhook documentation is a clear public example. It states that Stripe does not guarantee delivery of events in the order they were generated, and it retries failed deliveries in live mode for up to three days with exponential backoff. Read together, those two facts mean an older event that failed once can be redelivered long after newer events have been accepted. See Stripe's webhook documentation for the current wording.
The vendor is not the only source of reordering. Your own platform adds more:
- Several consumers reading the same stream in parallel.
- Partitioned brokers, where ordering holds within a partition and not across them. See the Apache Kafka documentation.
- FIFO queues, where ordering applies per message group, so the group key you choose defines your guarantee. See the Amazon SQS FIFO documentation.
- Dead-letter redrives and manual replays that put old messages back into a live flow. Our guide to dead letter queue triage, replay and closure covers that path.
- Backfills and migrations that write historical state through the same handler as live traffic.
Four problems that look alike
| Problem | What you see | Primary control |
|---|---|---|
| Duplicate delivery | The same event arrives twice | Tenant-scoped inbox keyed by source and event ID |
| Reordering | An older state arrives after a newer one | Source-provided ordering token checked at write time |
| Concurrent writers | Two workers or users change the same entity at once | Target-side conditional mutation |
| Unknown outcome | An outbound write times out and you cannot tell if it landed | Reconcile against authoritative operation state before any retry |
Treating these as one problem is the first design error. A table of seen event IDs solves exactly one row of the table above.
How Most Teams Handle Ordering, and Why It Fails
Key takeaway: Each common shortcut protects against one failure and quietly permits another. Know which one you are accepting.
Most teams start with the approach that works in the demo environment, where events arrive in order because one worker handles one sequence. The comparison below shows where each shortcut breaks.
| Common approach | Why it feels safe | Hidden failure mode | Acceptable when |
|---|---|---|---|
| Last event received wins | Simple, no extra state | A delayed retry overwrites newer state | Data is disposable and self-healing |
| Last timestamp wins | Looks like real ordering | Equal timestamps, clock skew and second-level precision produce wrong winners. Stripe advises against using its created field for ordering. | You own the clock and have a strict tie-breaker |
| Rely on a FIFO queue or partition | The broker "guarantees order" | The guarantee covers one hop and one key. It does not cover the source's retries, producer-side publishing, your group key choice or DLQ redrives. | Group key equals entity key, and replay is controlled |
| Read, compare, then write in code | Reads naturally and passes tests | Two workers can both read the old version, and both write | Only one writer can ever touch the entity |
| Ignore the payload and always refetch | Fetches current truth | Cost, rate limits, and a source read that may itself lag. Concurrent fetches can still write out of order. | Fetches are serialized per entity and the fetched state carries a version |
The pattern behind the failures
Every shortcut moves the ordering decision away from the place where the write happens. The check lives in a broker, a timestamp, a cache or application memory. The write lives in a database or a downstream API. Any gap between the check and the write is a window in which another actor can change the answer.
That is why read-compare-then-write is not compare-and-swap. A compare-and-swap is one atomic operation on the target: apply this change only if the current version is still X. If your target cannot do that, you are not doing compare-and-swap. You are serializing writers and hoping, and you should record that limitation in the design document.
For the retry side of this problem, see how our outage recovery framework for unknown-outcome operations treats reconciliation before replay.
A Proposed Design: Version-Gated Apply
Key takeaway: Record the delivery, gate the write on a source-provided version, and give every event a named outcome. This is a proposed design for your team to adapt. It is not an established CIS proprietary method.
The design has six steps. Each is simple. The value comes from doing all six together.
- Name the ordering token per entity. Use a monotonic version, sequence number or revision that the source assigns to each entity. If the source provides none, say so explicitly. Do not quietly substitute a timestamp.
- Record the delivery first. Insert an inbox row keyed by tenant, source and event ID, with a hash of the payload you received. A repeat delivery with the same ID and the same hash is a duplicate. The same ID with a different hash is a conflict to reject and investigate.
- Apply with a conditional mutation. The write includes the version condition, so the check and the change are one atomic operation on the target.
- Branch on the result. One row changed means applied. Zero rows changed means the event is stale, the entity is missing, or the claim failed. Each case has a different disposition.
- Park and reconcile gaps. If a child event arrives before its parent exists, park it with a bounded retry budget and fetch authoritative state. Parked is not a place events go to be forgotten.
- Commit work with the state change. Write the outbox row for downstream actions in the same transaction. Acknowledge the sender only after the commit succeeds.
Illustrative sketch (relational database)
This sketch is illustrative. It shows the shape of the pattern, not a drop-in implementation. Adapt names, isolation level and error handling to your platform.
-- Illustrative sketch (PostgreSQL-style). Not a drop-in implementation. BEGIN; -- 1. Record the delivery, scoped by tenant and source. INSERT INTO event_inbox (tenant_id, source, source_event_id, payload_sha256, state, received_at) VALUES (:tenant_id, :source, :event_id, :payload_hash, 'received', now()) ON CONFLICT (tenant_id, source, source_event_id) DO NOTHING; -- If 0 rows inserted: load the existing row. -- Different payload_sha256 -> reject as changed-payload reuse. -- Terminal state -> acknowledge without reprocessing. -- Claimed by another worker with a live lease -> do not apply; retry later. -- 2. Target-side conditional mutation: the ordering check and the write are one statement. UPDATE subscription SET status = :new_status, source_version = :event_version, updated_at = now() WHERE tenant_id = :tenant_id AND subscription_id = :subscription_id AND source_version < :event_version; -- 3. Branch on rows affected (application code): -- 1 row -> disposition 'applied' -- 0 rows -> entity exists with source_version >= :event_version -> 'superseded' -- entity does not exist -> 'parked' -- 4. Same transaction: schedule downstream work and record the disposition. INSERT INTO outbox (tenant_id, topic, entity_id, entity_version, created_at) VALUES (:tenant_id, 'subscription.changed', :subscription_id, :event_version, now()); UPDATE event_inbox SET state = :disposition, decided_at = now() WHERE tenant_id = :tenant_id AND source = :source AND source_event_id = :event_id; COMMIT; -- Acknowledge the sender only after this succeeds.Other targets, same principle
- Key-value and document stores. Amazon DynamoDB supports condition expressions on writes, and a failed condition returns an error you can map to "superseded". See the DynamoDB condition expression documentation.
- HTTP APIs you call. If the target API supports entity tags,
If-Matchmakes a write conditional, and a failed precondition returns 412. RFC 9110 also allows a 2xx response when the requested final state already exists, so your client must handle both. See RFC 9110. This standard governs requests you send. It does not describe how to receive webhooks. - Targets with no conditional write. Serialize writes per entity through a single-writer queue keyed by entity ID. Then state clearly that any other writer to that entity can still race you.
Notice what the design does not claim. It does not create ordering the source never provided, and it does not prevent every conflict in a multi-writer system. It makes stale writes detectable and rejectable at the moment of mutation, and it makes every outcome visible.
What This Means for the Enterprise Architect
Key takeaway: The architect owns the contract: ordering token, dispositions, tenant scope, ownership of failure and privacy of the stored record. These are decisions, not implementation details.
Decisions you need to own
| Decision | Typical owner | Evidence to ask for |
|---|---|---|
| Ordering token per entity type | Architecture with the source system owner | Written contract naming the field, its monotonic behavior and its scope |
| Disposition taxonomy and what each triggers | Architecture and operations | A documented state machine for inbox rows |
| Tenant-scoped keys for dedup and claims | Platform engineering | Unique constraints that include tenant and source, with a tested cross-tenant collision case |
| Retention and access for stored events | Data and privacy owner | Retention schedule, access model and deletion path |
| Terminal failure ownership | Service owner | Named queue owner, review interval and closure rules |
Define the dispositions
| Disposition | Meaning | Acknowledge sender? |
|---|---|---|
| Applied | Conditional write succeeded and outbox row committed | Yes, after commit |
| Superseded | A newer version already exists; this event is intentionally not applied | Yes, after recording |
| Parked | Prerequisite state is missing; bounded reconciliation is scheduled | Yes, only once the parked record is durable |
| Rejected | Signature, schema or payload-hash check failed | Per your security policy; never apply |
| Unknown | Completion could not be confirmed. This is neither success nor verified failure. | Not as success |
| Terminal failure | Reconciliation budget exhausted; a human owner must decide | Only after the case is durably assigned |
Uncertain input deserves its own mention. An event with an unrecognized schema version, an unverifiable signature or a missing ordering token should never be applied on a best guess. Quarantine it and let a named owner decide. For signature controls, see our webhook signature verification guide for CISOs.
The outbound side: never blindly retry an ambiguous timeout
The same discipline applies when your system writes to another system. If an outbound call to create a payment, issue a refund or change an entitlement times out, you do not know whether it happened. Retrying may duplicate a consequential action. Giving up may drop one. Use this sequence:
- Mark the operation unknown, not failed.
- Query the target's authoritative operation state using the operation's durable identity.
- Retry only if the target confirms it has no record of the operation, or if it honors an idempotency key bound to the same approved payload.
- Reject any reuse of the same key with a different payload.
- Claim the work atomically so two workers cannot send it at once.
Scope the operation identity to the tenant and bind it to a hash of the approved payload. A second legitimate refund on the same order is a new operation with a new identity. It is not a retry of the first. If your design cannot tell those apart, it will either block valid work or duplicate invalid work.
Identifiers are data too
Event IDs, entity IDs and tenant IDs can link activity to a person or an account. Payloads often contain far more. Decide what the inbox stores: a hash and minimal fields is often enough for dedup and ordering, while full payloads need a stated purpose, retention limit and access control. Legal obligations depend on your jurisdiction and your role, so involve counsel. Our guide to privacy governance for integration event logs goes deeper on retention and replay stores.
Why This Fails in the Real World
Key takeaway: Capable teams fail here because each component passes its own tests while the contract between components stays unwritten. The scenarios below are illustrative composites, not client results.
Scenario 1: The late retry that restored access (illustrative)
A SaaS billing source sends a "payment failed" event, then a "subscription cancelled" event. The first delivery of the earlier event fails at the receiver during a deployment and is retried hours later. The receiver applies events as they arrive, so the older state lands last. The customer's entitlement is restored after cancellation. Nothing errors. Dashboards show every event processed.
Why capable teams miss it: integration tests replayed events in creation order, so ordering was never exercised. The handler was idempotent, so it looked safe. Idempotency answers "did I see this before?" It does not answer "is this still true?"
Scenario 2: The FIFO queue that ordered the wrong thing (illustrative)
A team moves to a FIFO queue and concludes ordering is solved. The group key is the tenant ID, so one busy tenant becomes a single-lane bottleneck. To restore throughput, someone changes the key to a random value. Ordering within an entity quietly disappears. Months later, a DLQ redrive pushes old messages into the live queue behind newer ones.
Why capable teams miss it: the broker guarantee was real, but it covered one hop and one key. Nobody wrote down which key defined the ordering contract, so a performance fix could silently remove it.
Scenario 3: Two workers, one version (illustrative)
Two workers process events for the same entity. Each reads version 7, each checks that its event is newer, and each writes. The second write wins regardless of which event was newer. The code review passed because the compare logic was correct. The race lived between the read and the write.
The shared system gaps
- No written ordering contract between the source owner and the consumer owner.
- Metrics that count "processed" instead of applied, superseded, parked and unknown.
- Test fixtures that arrive in order, so reordering never appears before production.
- Split ownership. The platform team owns the queue, the domain team owns the table, and nobody owns the gap between them.
Our guide to integration failure testing before go-live lists drills that expose these gaps early.
Risks, Constraints, and Trade-Offs
Key takeaway: The pattern costs latency, storage and design effort. It also has hard limits when the source provides no usable ordering signal. Disclose both.
| Trade-off | What it costs | How to manage it |
|---|---|---|
| Version gating | Needs a source-provided token and an extra column per entity | Negotiate the token as part of the integration contract |
| Parking and reconciliation | Added latency and more states to monitor | Bound retries, set a review interval, and alert on aging |
| Per-entity serialization | Lower parallelism for hot entities | Partition by entity ID, not by tenant, and watch skew |
| Inbox storage | Storage growth and a new store of potentially sensitive identifiers | Store hashes and minimal fields; apply retention limits |
| Refetching authoritative state | API cost, rate limits and a source read that may lag | Fetch sparingly, carry the version, and treat the result as input to the gate |
Hard constraints to state plainly
- No ordering token from the source. You cannot detect staleness from the payload alone. Your options are fetch-and-compare against authoritative state, or accepting last-write-wins for that entity and documenting the risk.
- Targets without conditional writes. You can serialize your own writers, but you cannot prevent other writers from racing you. Record that as an accepted residual risk with an owner.
- Multiple sources for one entity. Version numbers from different systems are not comparable. You need a precedence rule or a single system of record.
- Delete and recreate cycles. A deleted-then-recreated entity can restart its version sequence. Decide how tombstones and generations are represented.
- Ordering is not the same as correctness. A correctly ordered stream of wrong events still produces wrong state. Pair this design with schema validation and reconciliation reports.
None of this argues for skipping the pattern. It argues for choosing which risks you carry on purpose.
A Lower-Risk Path to Production
Key takeaway: Start with the entities where a stale write costs the most, run the gate in shadow mode first, and require every control below to pass before launch.
A phased rollout
- Rank entities by consequence. Entitlements, payment status, inventory and credit limits come before profile fields.
- Write the ordering contract for the top three entity types, including the exact token and its scope.
- Run in shadow mode. Compute dispositions for live traffic without mutating state. Compare what the gate would have rejected with what your current handler applied.
- Enable the gate for one entity type, with a rollback path and on-call ownership.
- Run the drills: reordered delivery, duplicate delivery, concurrent workers, DLQ redrive, timeout on an outbound write, and cross-tenant key collision.
Go/no-go controls
These are mandatory, all-required checks. They are not a score, and a pass is not a production certification. It means the known gaps for this failure class are closed. Any single no-go blocks launch.
| Control | Go condition | No-go signal |
|---|---|---|
| Ordering token | Source token named, documented and monotonic per entity | Timestamps used as a stand-in |
| Atomic ordering check | Version condition is part of the target-side write | Read, compare, then write in application code |
| Tenant-scoped identity | Dedup and claim keys include tenant and source | Global event ID uniqueness assumed |
| Changed-payload rejection | Same ID with a different payload hash is rejected | Payload ignored once the ID has been seen |
| Atomic recording | Dedup record, state change and outbox row commit together, or recovery is explicit | Acknowledgment sent before the commit |
| Worker claim | Atomic claim with a lease prevents concurrent processing | Two workers can apply the same event |
| Disposition visibility | Dashboards show applied, superseded, parked, rejected, unknown and terminal counts | Only "processed" is counted |
| Unknown handling | Ambiguous outbound timeouts reconcile against authoritative state before any retry | Blind retry on timeout |
| Terminal failure ownership | Named owner, review interval and closure rule | Parked items age with no owner |
| Privacy review | Retention, access and minimization decided for stored events and identifiers | Full payloads kept indefinitely by default |
| Drill evidence | Reorder, duplicate, concurrency and redrive drills pass in a production-like environment | Only in-order happy-path tests exist |
2027 Update
For 2027 planning, re-check each source's delivery documentation before you rely on it. At the time of writing, Stripe's public documentation still states that event order is not guaranteed and that duplicates can occur, and broker documentation still scopes ordering to a partition or message group. Vendors change behavior, so treat their documentation as an input to your review calendar. The principle outlasts any one vendor: ordering is a property you verify at the point of write, not one you inherit from the transport. Teams that add AI agents or automated workflows as event producers and consumers should apply the same gates, because those actors multiply the number of concurrent writers.
Is your integration layer applying events, or just accepting them?
Stale writes rarely throw errors. They show up later as wrong balances, wrong access and wrong reports.
Ask CIS integration architects to review your ordering, idempotency and reconciliation design.
Request Free ConsultationIf you want a second set of eyes on the contract, explore CIS's enterprise integration and API services and enterprise architecture and roadmapping.
Conclusion: Decisions to Make This Quarter
Out-of-order delivery is a design input, not an incident. The practical question is whether your write path can tell a stale event from a current one, and whether you can prove it. Five actions to take next:
- Inventory your consequential entities and write down the ordering token for each. Where none exists, record the accepted risk and its owner.
- Move the version check into the write. Replace any read-compare-then-write path on a consequential entity with a conditional mutation, or document the serialization limits you are relying on.
- Adopt a disposition taxonomy and make dashboards report applied, superseded, parked, rejected, unknown and terminal counts.
- Run the reorder, duplicate, concurrency and redrive drills before the next release that touches an integration.
- Review what the inbox stores, including identifiers, with your privacy owner.
About the CIS Expert Team
Cyber Infrastructure (CIS) is an AI-enabled software development and IT solutions company established in 2003, with more than 1,000 professionals serving clients in over 100 countries. CIS describes its delivery processes as CMMI Level 5-appraised, ISO 27001 and SOC 2-aligned. When you evaluate any vendor, including CIS, ask for current certificates and their scope. This article has been reviewed by the CIS Expert Team.
Frequently Asked Questions
What is an out-of-order event in an integration?
It is an event that arrives after a later event for the same entity. A retry, a parallel consumer, a partition boundary or a replay can all cause it. If the handler applies events in arrival order, the older state can overwrite the newer state.
Is an idempotent handler enough to protect against out-of-order events?
No. Idempotency prevents the same event from being applied twice. It does not tell you whether an event is still current. You need an ordering check, ideally a version condition inside the write itself.
Can I use event timestamps to decide which event is newer?
Usually not safely. Timestamps can tie, can have coarse precision and can reflect different clocks. Stripe's documentation, for example, says its event timestamps are recorded in seconds, that different events can share a timestamp, and that you should not use them to determine order. Prefer a source-provided version or sequence number.
Does a FIFO queue or Kafka partition solve ordering end to end?
Not by itself. These systems scope ordering to a message group or partition, and the key you choose defines the guarantee. They do not control how the source publishes or retries, and a DLQ redrive can reintroduce old messages behind newer ones.
- Choose a key that matches the entity, not just the tenant.
- Control redrive and replay explicitly.
- Still check the version at write time.
What should I do when the source provides no ordering token?
You have two honest options. Fetch authoritative state and compare it before writing, with the limits that implies for cost and freshness. Or accept last-write-wins for low-consequence data and document the risk. Do not invent an ordering signal from timestamps and present it as a guarantee.
What should I do when an outbound write times out?
Treat the result as unknown. Query the target's authoritative operation state using the operation's durable identity. Retry only if the target confirms it has no record, or if it honors an idempotency key bound to the same approved payload. Reject key reuse with a changed payload.
When should we acknowledge a webhook to the sender?
Only after the delivery is durably recorded and the resulting state change and scheduled work are committed together, or are explicitly recoverable. Acknowledging earlier risks losing the event if your process fails between the response and the commit.
Is the Version-Gated Apply pattern a CIS proprietary framework?
No. It is a proposed design that combines widely used techniques: inbox recording, optimistic concurrency and outbox scheduling. Adapt it to your platform, and validate it with your own drills.
Old events should never overwrite new truth.
If your integrations touch billing, entitlements, inventory or customer records, an ordering gap is a business risk. Close it before it shows up in a reconciliation report.
Talk to CIS about an integration design review, from ordering contracts to drill plans.
Request Free ConsultationIntegration-consulting-services
This article is most relevant for technology and digital-transformation leaders who need to roll out a practical technology solution. Use the related CISIN path to compare delivery options, implementation fit, risk, and practical next steps.
Reviewed for technology and business decision makers
This guide is reviewed for clarity, technical and operational relevance, service alignment, and a useful next step.
Validate legal, security, data, budget, and operational requirements with the relevant stakeholders before rollout.

