Designing Webhooks for Duplicates and Delayed Events

Build webhook receivers that verify input, accept work durably, handle duplicate deliveries, and reconcile state when events arrive late.

A webhook is a notification from another system. It is tempting to treat that notification as a perfectly ordered instruction: receive it, update a record, and return success. Real integrations need a more careful contract.

The same event may arrive more than once. A later event may arrive first. The sender may retry because it never received the response, even when the receiver completed its work. I design around those conditions from the beginning.

Keep the receiving boundary small

The receiver should authenticate the delivery, validate its basic shape, record it durably, and acknowledge it according to the provider’s protocol. Expensive business processing can happen afterward.

Signature verification must use the representation required by the provider. Where the signature covers the original request bytes, parsing and reserializing JSON first can change what is being verified. Verification failures should never fall through into normal processing.

Durability is equally important. Returning success before recording the event creates a window in which the process can crash and lose work that the sender believes was accepted. An in-memory queue does not close that window.

Deduplicate receipts and business actions

I record the provider, connected account, and event identifier under a database uniqueness constraint. That lets concurrent duplicate deliveries converge on one stored receipt.

However, an event ID is not always the same as a business operation ID. Two distinct events may legitimately describe the same resource or cause the same downstream action. The business action needs its own identity and transition rules.

Stripe’s webhook documentation explicitly describes duplicate deliveries and does not guarantee delivery order. I use those documented behaviors as input to the receiver design, rather than assuming the timing observed in a development session is a contract.

Model state transitions instead of arrival order

Consider a subscription integration. A delayed notification should not blindly overwrite a more recent state. Sometimes the right response is to retrieve the authoritative current resource and reconcile the local view.

For operations that require historical transitions, I preserve the event and apply domain-specific rules. A timestamp alone may be insufficient to establish order, especially when several changes share the same timestamp or different systems use different clocks.

The decision depends on the purpose of the data. A local status badge may only need current state. A ledger or audit history needs the actual sequence and meaning of changes. One generic “last write wins” handler cannot safely serve both purposes.

Make processing failures visible

I separate receipt status from processing status. An accepted event may still be pending, retrying, completed, or awaiting investigation. This distinction prevents a healthy HTTP response rate from hiding a broken integration.

Each processing attempt should have a bounded retry policy. When work cannot progress, a review or dead-letter queue should retain the event reference and failure reason. Replaying it should use the same deduplication and authorization rules as the original attempt.

Useful metrics include the age of the oldest unprocessed event, processing latency, repeated failures, and reconciliation mismatches. They describe the customer’s actual delay more clearly than the total number of webhook requests.

Test what the network can do

My test sequence includes duplicate delivery, reversed arrival order, a crash after durable receipt, and a timeout after an external side effect. I also check that an invalid signature cannot create even a pending business operation.

A well-designed webhook receiver gives the team two useful answers: whether the notification was safely accepted, and whether its intended business effect was completed. Keeping those answers separate makes the integration much easier to operate.

Updated 25 September 2026.