Skip to content

Webhook Ordering Guarantees — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Webhook Ordering Guarantees. We cover key concepts, practical examples, and best practices to help you master this topic.

Webhook delivery ordering guarantees describe whether events reach a consumer in the same sequence they were produced. Most webhook systems provide at-least-once delivery without strict ordering, which means a consumer may receive event B before event A. This lesson examines ordering models, sequencing strategies, and how to build idempotent consumers that handle out-of-order delivery.

What You'll Learn

  • Understand at-least-once, at-most-once, and exactly-once delivery models
  • Implement sequence numbers and causal ordering
  • Handle out-of-order delivery with idempotent consumers
  • Manage ordering in retry and fan-out scenarios

Why It Matters

Without ordering guarantees, dependent events can arrive in the wrong sequence. A user.deleted event arriving before user.updated will cause a 404. Understanding ordering lets you build robust consumers that process events correctly regardless of arrival order.

Real-World Use

  • GitHub sends webhook deliveries with an X-GitHub-Delivery header but no ordering; consumers use the event payload timestamp
  • Stripe includes id and created fields so consumers can sequence events
  • Shopify uses sequential delivery IDs within a store scope
  • Payment gateways guarantee order of transaction lifecycle events (authorized -> captured -> settled)

Mermaid Flow

graph LR
    A[Event Produced] --> B[Queue / Broker]
    B --> C{Ordering Strategy}
    C -->|Single Partition| D[FIFO Delivery]
    C -->|Sequence Numbers| E[Consumer Reorders]
    C -->|Timestamps| F[Best Effort Sort]
    D --> G[Consumer]
    E --> G
    F --> G
    G --> H[Processed in Order]

Teacher's Corner

Compare webhook ordering to message queue ordering. Webhooks are HTTP push-based, so the provider has less control over delivery order compared to a pull-based Message Broker. Stress that webhook consumers must assume out-of-order delivery and design accordingly. Explain that strict FIFO ordering in Distributed Systems comes with significant throughput trade-offs.

Code Examples

Example 1: Sequence Number Tracking in Consumer

import json
from flask import Flask, request

app = Flask(__name__)

last_sequence = {}

@app.route("/webhook", methods=["POST"])
def webhook():
    payload = request.get_json()
    event_type = payload["event_type"]
    sequence = payload["sequence"]
    event_id = payload["event_id"]

    key = f"{event_type}"
    if key in last_sequence and sequence <= last_sequence[key]:
        return ("Duplicate or out of order", 200)

    last_sequence[key] = sequence

    print(f"Processing event {event_id} seq={sequence}")
    return ("OK", 200)

if __name__ == "__main__":
    app.run(port=5000)

Expected Output: When event A seq=1 arrives then event B seq=2, both process. If event A arrives again with seq=1, it is rejected as duplicate.

Example 2: Causal Ordering with Parent Event ID

import sqlite3

def insert_event(conn, event_id, parent_id, payload):
    if parent_id:
        parent = conn.execute(
            "SELECT processed FROM events WHERE event_id = ?",
            (parent_id,)
        ).fetchone()
        if parent and not parent[0]:
            return {"status": "deferred", "reason": "parent not processed"}

    conn.execute(
        "INSERT INTO events (event_id, parent_id, payload, processed) VALUES (?, ?, ?, 1)",
        (event_id, parent_id, payload)
    )
    conn.commit()
    return {"status": "processed"}

events = [
    ("evt-3", "evt-1", '{"action": "update"}'),
    ("evt-1", None, '{"action": "create"}'),
    ("evt-2", "evt-1", '{"action": "verify"}'),
]

conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE events (event_id text, parent_id text, payload text, processed int)")

for eid, pid, payload in events:
    result = insert_event(conn, eid, pid, payload)
    print(f"{eid}: {result['status']}")

Expected Output: evt-3: deferred; evt-1: processed; evt-2: processed

Example 3: Reorder Buffer Implementation

import heapq
import threading

class ReorderBuffer:
    def __init__(self, max_disorder=100):
        self.buffer = {}
        self.next_expected = 1
        self.lock = threading.Lock()
        self.max_disorder = max_disorder

    def receive(self, sequence, event):
        with self.lock:
            if sequence < self.next_expected:
                return {"action": "ignore", "reason": "already processed"}
            if sequence == self.next_expected:
                self.next_expected += 1
                ready = [event]
                while self.next_expected in self.buffer:
                    ready.append(self.buffer.pop(self.next_expected))
                    self.next_expected += 1
                return {"action": "deliver", "events": ready}
            if len(self.buffer) >= self.max_disorder:
                return {"action": "error", "reason": "buffer full"}
            self.buffer[sequence] = event
            return {"action": "buffered", "position": sequence - self.next_expected}

buf = ReorderBuffer()
print(buf.receive(3, "event-c"))
print(buf.receive(1, "event-a"))
print(buf.receive(2, "event-b"))

Expected Output: {'action': 'buffered', 'position': 2}; {'action': 'deliver', 'events': ['event-a']}; {'action': 'deliver', 'events': ['event-b', 'event-c']}

Common Mistakes

  1. Assuming webhooks arrive in order across different event types
  2. Blocking processing of later events while waiting for a missing earlier event indefinitely
  3. Using timestamps alone for ordering without accounting for clock skew
  4. Not setting a maximum reorder buffer size, leading to memory exhaustion
  5. Rejecting out-of-order events instead of buffering and reordering them
  6. Ignoring the case where a retry delivers an older event after a newer one was already processed

Practice Questions

  1. What is the difference between at-least-once and exactly-once delivery in webhooks?
  2. How would you handle a scenario where event 5 arrives but event 4 is still missing?
  3. Why is strict FIFO ordering difficult to achieve across distributed webhook providers?
  4. How can idempotency keys help with out-of-order delivery?
  5. Challenge: Design a webhook consumer that processes stock trade events in strict order by trade ID, using a buffer that can hold up to 1000 out-of-order events and times out after 30 seconds.
Answer Key 1. At-least-once delivers each event one or more times; exactly-once guarantees each event is processed exactly once. Webhooks typically use at-least-once. 2. Buffer event 5 and wait for event 4. Set a timeout; if event 4 never arrives, check for producer-side issues or manual intervention. 3. Strict FIFO across distributed systems requires consensus mechanisms (Paxos, Raft) or single-partition brokers, both of which limit throughput and add latency. 4. Idempotency keys allow safe reprocessing: if an event arrives out of order but was already processed (by its idempotency key), the consumer can safely ignore it. 5. Use a priority queue keyed by trade ID, a configurable max buffer size, a TTL per buffered event. When the buffer fills or TTL expires, reject or dead-letter the gap events.

FAQ

Can webhooks ever guarantee exactly-once delivery?

True exactly-once requires distributed transactions or two-phase commit, which most webhook providers do not implement. At-least-once with idempotent consumers is the practical standard.

How do I detect missing webhook events in a sequence?

Track expected sequence numbers. When a gap is detected, query the provider's audit log or event API to fetch the missing event. Implement a periodic gap-detection job.

Does using a message queue like RabbitMQ help with webhook ordering?

Yes. If you route all webhook events for a given scope (e.g., a tenant) through a single queue partition, you can preserve delivery order. This shifts the ordering responsibility to the queue.

What is the throughput cost of strict ordering?

Strict FIFO can reduce throughput by 10x or more compared to unordered delivery because it prevents parallel processing and requires acknowledgment before delivering the next event.

Should I reject or buffer out-of-order events?

Buffering is preferred because it allows eventual ordering. Rejecting causes the provider to retry, which leads to more network traffic and potential thundering herd problems.

How do sequence number overflows work?

Use a monotonically increasing 64-bit integer for sequence numbers. It will not overflow in practice. If you must use smaller numbers, implement a wrap-around detection mechanism.

Mini Project

Build a webhook ordering demonstration using a simple Python server. Create three endpoints: /produce generates events with incrementing sequence numbers, /webhook receives events and attempts to reorder them in a buffer, and /status shows the current state of the buffer and processed events. Simulate out-of-order delivery by randomly delaying and reordering events before sending. Measure the time it takes for all events to be correctly ordered and processed.

What's Next

Now that you understand ordering guarantees, learn about building a webhook provider to see how to implement reliable delivery with proper ordering on the producer side.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro