Why Webhook Events Arrive Out of Order
A webhook sends a notice from one service to another when something changes, such as a customer record or an order. Events…
A webhook is an HTTP request that a service sends to your service when an event occurs, such as a repository change or an account update. The service that sends the request is called the provider. The receiver’s job is to authenticate that request, make the event safe from process failure, and acknowledge it promptly. Acknowledgment should mean “accepted for processing,” not “all related work is finished.”
That distinction determines what the handler does first. It should verify the request, durably save or queue the event, and then return the success response the provider expects. A separate worker can perform slower business tasks afterward. The right response code, timeout, and retry behavior depend on the provider.
The receiver starts when the provider sends an HTTP POST request to its configured endpoint. The handler reads the request headers and body, then checks the request before treating its contents as trusted data.
If the provider signs the request, the handler must verify the signature using the body in the form the provider signed. A signature is a value computed from the request and a shared secret; checking it helps confirm that the request came from someone with that secret and that the signed content was not changed. For example, GitHub uses an HMAC-SHA256 signature in X-Hub-Signature-256 and recommends a constant-time comparison. Re-serializing the body before verification can produce a different signature, so the handler should preserve the original bytes for this check. See GitHub’s signature validation guidance.
After verifying the request, the handler checks what it needs to accept the event safely. It identifies the event type and action, then checks that the receiver supports them. If the provider supplies an event identifier, the handler saves it. These checks let the receiver reject malformed or unsupported requests without delaying the response for slower business tasks.
Next, the receiver saves the authenticated event in storage that survives a process restart or puts it on a durable queue, so accepted work remains available if the handler stops or restarts. An in-memory handoff alone does not provide that protection: if the handler acknowledges the request and then crashes before the event survives, the provider believes delivery succeeded while the receiver has lost the work.
The handler should keep the request path short, but it must wait for the event to be saved durably. After the write succeeds, the handler returns the success response the provider requires without waiting for later work.
The provider observes the HTTP response, not whether the receiver later completed a payment, sent an email, or updated another service. If the receiver waits for those tasks, the time they take counts against the provider’s delivery window. A slow response can trigger a retry or other provider-specific failure handling.
The exact rules differ. GitHub expects a 2xx response within 10 seconds, but its documentation says failed deliveries are not automatically redelivered. Plaid retries certain failures for up to 24 hours, and treats some response codes differently. The receiver must follow the provider’s documented response and recovery rules because webhook providers do not all handle deliveries the same way. See GitHub’s delivery recommendations and Plaid’s webhook behavior.
The receiver should acknowledge only after it has safely accepted the event. If storage or queueing fails, it should return the failure response specified by the provider rather than claim success. That response does not guarantee a retry: GitHub, for example, does not automatically redeliver failed deliveries. Operators need a recovery path for events that were not accepted.
Conversely, if the durable write succeeds but the response gets lost, the provider may send the event again. That is a normal delivery failure mode, so the receiver must be prepared to recognize duplicates.
After the handler saves the event durably and returns success, a worker can load it, choose what to do based on its event type, and perform the related business tasks. The worker should handle only event types and actions the receiver supports. Providers can change the data they send, so the worker should check the fields it uses instead of assuming every event has the same structure.
The worker also needs idempotency. An operation is idempotent when repeating it produces no additional unintended effect. For example, if a provider sends the same event twice, the receiver should not charge a customer twice or send a duplicate notification. Store an event identifier the provider supports and use it to detect repeats. Also make later actions safe to repeat. OWASP’s webhook security guidance recommends deduplicating event IDs and designing side effects to be idempotent.
Verify the request with the provider’s signature scheme before using an identifier to deduplicate events. GitHub documents X-GitHub-Delivery as useful for recognizing redeliveries, but that header is not itself a signed event ID. Use the provider’s signature scheme to authenticate the request, then use identifiers according to the provider’s documented guarantees.
The worker should also account for events arriving out of order. Receipt order does not necessarily match the order in which the underlying changes happened. When an older event could overwrite newer data, the worker should check the current state of that object in the provider’s service, if the provider offers a suitable way to check it. A timestamp alone does not establish an ordering guarantee. GitHub’s troubleshooting guidance and Plaid’s documentation both warn about out-of-order delivery.
Finally, account for missed events as well as duplicates. Providers differ in how long they retry and whether they let you request another delivery. Plaid documents a way to list recent webhook events so you can compare its event history with your records. GitHub requires you to recover failed deliveries manually or with a script. Check the provider’s documented recovery options and compare its event history with the events your receiver has stored.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com