Book a 30-min callBook a call

System design: a reliable webhook delivery system

How to design webhook delivery: an outbox and a queue, retries with backoff and jitter, HMAC signatures, SSRF checks, dead letters, replay and delivery logs.

An event card flowing through a queue of blocks to three worker dots that post to customer endpoint cards, a padlock on one arrow and a dashed loop returning a failed delivery.
Written by
BYD Tech
Published
Topic
Deep dives
Reading time
15 min read

Questions people ask

Should webhooks be delivered in order?

Usually not as a guarantee. Strict order per endpoint means one failing event blocks the rest for hours of retries. Stripe does not guarantee order and tells receivers not to depend on it. Give each event a version or sequence number, or send thin events so receivers fetch the latest state.

How long should a webhook sender keep retrying?

Long enough to cover an overnight or weekend-length outage at the receiver. Stripe retries for up to three days in live mode, and the Standard Webhooks example schedule spans a little over three days. After the deadline, keep the delivery as failed and let the customer replay it.

Why sign the timestamp as well as the body?

A signature over the body alone stays valid forever, so anyone who captures a request can resend it later. Signing a timestamp, and rejecting requests outside a tolerance such as 5 minutes, limits that window. Deduping on the event id stops replays inside it.

Is HTTPS enough without a signature?

No. HTTPS protects the request in transit, but anyone who knows or guesses the URL can send a request to it. The signature proves the request came from the holder of the secret. Use both: Standard Webhooks notes that signatures prevent tampering but do not encrypt anything.

What status code should a webhook receiver return?

A 2xx as soon as the event is verified and stored, before any slow processing. Senders treat everything else, including redirects and timeouts, as a failure and retry. Under Standard Webhooks, a 410 Gone tells the sender to disable the endpoint.

Building something like this?