Handling Out-of-Order Webhook Events
Handling out-of-order webhook events safely
Webhooks can arrive out of order — retries, network delays, and independent endpoint runs mean you must design consumers to handle late or duplicate events. This guide explains practical Laravel patterns to handle out-of-order webhook events and keep state correct.
You will implement an apply-if-newer pattern (store occurred_at), add idempotency/dedupe, and test locally and end-to-end. The primary keyword “handle out of order webhook events” is used in the recommended design.
Why ordering is not guaranteed
Understand the sender behavior so you don’t assume ordering.
Retries use exponential backoff (late arrivals happen)
Retries are normal and will cause older attempts to show up after newer ones.
Multiple endpoints + independent runs per endpoint
Different endpoints or consumers may process events at different speeds, producing apparent reordering.
Micro checklist:
- Expect at-least-once delivery semantics.
- Do not assume monotonically increasing arrival times.
- Store
occurred_atin your consumer to compare event freshness.
Trigger two events from the Sample Project and confirm attempts + timing in Message Log.
Choose your ordering contract
Pick one contract and implement it consistently across services.
Strict ordering (rarely worth it)
Requires global coordination (locks, single-queue). Only choose when business logic absolutely depends on strict ordering.
“Last-write-wins with versions”
Store a version/timestamp and apply updates only if the incoming event is newer.
Event-sourcing style apply-if-newer
Persist events to an inbox and project state from the canonical event stream; apply only when appropriate.
Common gotcha: Treating the first-arrived event as canonical—use
occurred_ator an explicit version to decide.
Minimum safe design
Implement these minimum protections to remain correct under retries and reordering.
Idempotency / dedupe for at-least-once delivery
Use a dedupe key (message id or hash of payload) to ensure side effects are not applied more than once.
Store occurred_at and “entity version”
Keep the event timestamp or version in your state so you can reject or ignore older events.
Micro checklist:
- Persist the raw event to an inbox table.
- Record
occurred_aton state rows.- Compare and apply only if incoming
occurred_at> currentlast_event_at.
Laravel reference implementation
Practical, minimal example: store inbound events and apply only if newer.
Inbox table
Store raw events in webhook_inbox for audit and replay.
Apply rules with optimistic checks
| |
Suggested diagram: A sequence showing “deliveries (t1,t2,t3)” arriving out of order, write to inbox, then project state only if
occurred_atis newer.
Testing + failure modes
A) Direct local test (simulate out-of-order)
Send newer then older (same entity_id) and confirm state remains at the newer timestamp:
| |
Expected: state remains at 10:05:00Z.
B) SendPromptly end-to-end validation
Trigger events via the API and confirm delivery runs in Message Log.
Common failure modes
- Assuming order ⇒ “pending” overwrites “paid”.
- No idempotency ⇒ retries duplicate side effects.
- No stored
occurred_at⇒ can’t resolve late arrivals safely. - Processing inline (slow) ⇒ timeouts ⇒ retries ⇒ even more reordering.
- Mixed clocks / wrong timezone parsing ⇒ incorrect comparisons.
- Rejecting older events with 5xx ⇒ pointless retries.
Related
- Retries and success criteria (2xx)
- Idempotency model (24-hour TTL) for ingestion
- Dedupe patterns for webhook consumers
- Debugging replays in logs
If state looks wrong, inspect per-attempt timing in Message Log and verify your apply-if-newer rule.
Conclusion
- Expect at-least-once delivery and design for out-of-order arrivals.
- Persist raw events to an inbox and project state using
occurred_atcomparisons. - Prefer apply-if-newer (LWW) or event-sourced replays over strict ordering.
- Keep processing fast and idempotent to avoid retry storms.
- Use Message Log and the Sample Project for deterministic tests.