← All articles

Exponential Backoff and Jitter for Webhook Retries

Webhooker Team Updated 12 min read
Flat illustration of exponential backoff with jitter: bounce arcs of doubling width scattering dots at each landing, the last ending at a blue pulse node.

Webhook retries should wait longer after each failure instead of firing at a fixed interval. Use exponential backoff and retry with jitter so attempts spread out, retry 5xx, 408, 429 and network errors, honor Retry-After, and open a circuit breaker when a destination keeps failing. Cap the attempts, then move dead deliveries to a queue for replay.

Retry logic is often written as an afterthought, yet it decides whether a short destination outage stays short. One failed webhook is normal. A retry policy that turns that failure into a self-inflicted outage is a design bug. Below are the parts of a retry strategy that holds up when a destination goes down: exponential backoff, jitter, conditional retries by status code, and a per-destination circuit breaker. If the request-and-acknowledge exchange underneath is unfamiliar, here is one webhook delivery in full. Everything here sits at the gateway-to-destination layer. Whatever retry logic your consumer runs internally is a separate concern, so keep the two apart as you read.

Why naive retries make things worse

The most common webhook retry policy is also the worst one: on failure, wait a fixed number of seconds and try again, forever. It looks harmless in a test with one event and does real damage during an incident.

Picture a destination that goes down at 14:00. Every webhook you tried to deliver in that window failed. If your retry interval is a flat 30 seconds, all of those failed deliveries now retry together, every 30 seconds, in lockstep. The destination comes back up, still cold, still reconnecting to its own database, and gets hit by the entire backlog at once. That synchronized wall of retries is the thundering herd, and it frequently knocks the destination straight back down. Your retry logic has now extended the outage it was supposed to survive.

Fixed-interval retries fail for two reasons at once. They do not give a struggling destination room to recover, and they align every retrying client onto the same clock, so the load arrives in spikes. Backoff fixes the first problem and jitter fixes the second, which is why you need both.

Exponential backoff

Exponential backoff means the wait between attempts grows multiplicatively instead of staying flat. Each retry roughly doubles the delay: 1 second, then 2, then 4, then 8, and so on. A destination that is briefly overloaded gets a short pause. A destination that is fully down gets progressively longer gaps, so you stop pounding on it while it recovers.

The formula is simple. The delay for a given attempt is the base interval multiplied by two raised to the attempt number, clamped to a maximum:

delay = min(base * 2 ^ attempt, cap)

Three parameters shape the curve, and each depends on your traffic:

The table below shows one plausible backoff schedule. Treat it as a starting point and tune it against your own destinations.

Table: example exponential backoff schedule with a base of 1 second and a cap of 60 seconds, showing how the delay doubles each attempt until it hits the ceiling.

AttemptRaw delay (base 1s)Applied delay (cap 60s)
11s1s
22s2s
34s4s
48s8s
516s16s
632s32s
764s60s
8128s60s

There is no single correct curve here. A payment provider that needs fast confirmation wants a tight base and a low cap. A batch sync that nobody is watching in real time can afford long gaps and many attempts. Stripe, for comparison, spreads its live-mode retries over three days, and what that window means for your endpoint is less generous than it sounds. Pick the shape from what each destination needs instead of copying one curve everywhere.

Jitter

Backoff on its own fixes the recovery-room problem but leaves the synchronization problem half-solved. Imagine a thousand deliveries that all failed at the same instant. With pure exponential backoff, every one of them waits exactly one second, then exactly two, then exactly four. They double in unison and re-collide at every step. The spikes are further apart, and each one is just as tall.

Jitter breaks the lockstep by adding randomness to each wait. Instead of every client retrying at exactly the four-second mark, each picks a random point in a window, so the load smears out into something the destination can absorb. The AWS Architecture Blog’s write-up on exponential backoff and jitter is the canonical reference. Its simulations show that adding jitter reduces contention and completes the work faster.

The two common variants:

For most webhook systems, full jitter is plenty. Any reasonable jitter beats none, so don’t spend long choosing between them.

Conditional retries

Not every failure deserves a retry. Retrying blindly wastes work in the best case and causes damage in the worst. The status code the destination returns tells you whether trying again could plausibly help.

Redirects are a policy choice. Webhooker never follows a redirect, so it treats a 3xx as a failed attempt and retries it; the fix is to point the destination at its final URL (see the delivery docs).

The common mistake is treating all failures as equal. A destination that returns 422 for a bad payload does not want to see that payload seven more times. Send those straight to your dead letter queue instead of burning attempts on a request that can never succeed.

Circuit breakers

Backoff and jitter smooth out retries, but they still assume the destination will eventually answer. When a destination has been failing for minutes, continuing to send it traffic, even politely spaced traffic, wastes your workers and adds load to a service that is already struggling. A circuit breaker is the switch that stops sending.

The pattern borrows three states from electrical circuits. Closed is normal: traffic flows. When failures to a destination cross a threshold, the breaker trips to open, and further deliveries to that destination are held back immediately instead of being attempted and failing. After a cooldown, the breaker moves to half-open and lets a small number of probe requests through. If those succeed, it closes and normal delivery resumes. If they fail, it opens again and waits longer.

A per-destination breaker gives you isolation. One destination melting down cannot consume the workers, connections and retry budget your healthy destinations depend on. Without a breaker, a single dead endpoint can starve the whole delivery pipeline as workers pile up waiting on timeouts. Isolation at the queue layer matters just as much: competing workers have to claim deliveries without blocking each other, which is what FOR UPDATE SKIP LOCKED buys you.

Webhooker builds retries with exponential backoff and a per-destination circuit breaker into delivery, so a flaky destination is backed off and isolated without slowing traffic to the others. The retries and replay docs show what that looks like from your endpoint’s side.

Retries imply duplicates

Once you retry, you accept the chance of delivering the same event twice. Webhooker’s delivery queue is at-least-once by design, and at-least-once is the right guarantee for webhooks. The alternative, at-most-once, drops an event whenever a delivery is uncertain, and losing webhooks without a trace is far worse than an occasional duplicate.

Retries have a second consequence that is easier to miss: any backoff curve is also a reordering function. An event that fails and finally lands eight seconds later arrives behind events created after it, which is why webhooks arrive out of order even when the provider sent them in sequence.

Consider the classic case: your gateway delivers the webhook, the destination processes it, and the acknowledgement is lost on the way back. From the gateway’s side the delivery looks failed, so it retries, and the destination sees the same event a second time. No backoff curve prevents this. The sender cannot tell a lost request from a lost response.

So the fix belongs in the consumer, which must be idempotent: processing the same event twice must produce the same result as processing it once. The standard mechanism is a stable event identifier that the consumer records and checks before acting. The implementation is covered in make retried deliveries safe with idempotency keys. A retry strategy is only safe when it is paired with an idempotent consumer.

When to give up

Retries should not run forever. Past a certain number of attempts, a failing delivery usually points at something a machine cannot fix on its own, such as a misconfigured destination or an endpoint that moved. Retrying beyond that point buries the problem under log noise.

Where to stop varies a lot between the big providers. Checked against each vendor’s docs on 14 September 2026:

SenderAutomatic retriesWindowAfter the last attempt
Stripe (live mode)Exponential backoffUp to 3 daysManual resend from the dashboard
Stripe (sandbox)3 retriesA few hoursManual resend
GitHubNone—Manual redelivery within 3 days
ShopifyUp to 8 retries4 hoursSubscription can be removed if failures persist
Slack Events API3 retries: at once, after 1 minute, after 5 minutesAbout 5 minutesNo more retries, unless Delayed Events adds hourly retries for 24 hours
Webhooker6 attempts: at once, then 30 s, 2 min, 10 min, 1 h, 4 hAbout 5 hoursDead-letter queue with one-click and bulk replay

So the answer runs from zero automatic retries at GitHub to three days at Stripe. Your receiver cannot count on any one of them.

When a delivery exhausts its attempts, it should stop retrying and move somewhere durable and visible. That place is the dead letter queue. In Webhooker, a dead delivery keeps its full per-attempt history and response logs, so you can see what each destination returned before you decide what to do. Once the underlying issue is fixed, you replay it, one delivery at a time or in bulk.

Keep failed deliveries instead of discarding them. A pipeline that drops events after the last retry leaves no record of what it lost, so the gap tends to surface days later as mismatched data in some downstream system. Webhooker includes durable, retrying delivery on every plan, since it is the part of a webhook gateway you cannot bolt on later. If you compare vendors, check whether each retry attempt is billed, because that puts someone else’s outage on your invoice. Whether to write all of this yourself is a separate question, with costs laid out in build vs buy for webhook infrastructure.

The next piece to design is the dead letter queue itself. For how to inspect what failed, fix the cause and replay without creating duplicates, read where deliveries go after the last retry fails. For the receiving side as a whole, including what to do after an outage longer than any retry window, see webhook retries and replay.

Frequently asked questions

How many retries is enough?

There is no universal number. Slack retries 3 times over about 5 minutes, Shopify up to 8 times over 4 hours, Stripe for up to 3 days in live mode, and Webhooker makes 6 attempts over about 5 hours. A window of a few hours rides out most deploys, restarts and short outages without holding events for days. Time-sensitive traffic wants fewer, faster attempts; background syncs can afford more. Once you exhaust the count, send the delivery to a dead letter queue rather than retrying forever.

What’s a good backoff cap?

The cap is the longest single wait between attempts, and it depends on how quickly the event needs to land. Many systems set it between 30 seconds and a few minutes. A low cap keeps retries responsive for time-sensitive deliveries; a higher cap eases pressure on a destination during a long outage. Always pair the cap with jitter, or every capped retry fires at the same moment and rebuilds the thundering herd you were trying to avoid.

Should I retry 429s?

Yes, but carefully. A 429 means the destination is rate-limiting you, so retrying is correct, but retrying too eagerly makes it worse. Back off harder than usual, and if the response includes a Retry-After header, honor that value instead of your default schedule, because the server is telling you when it is ready. A persistent 429 also means your overall send rate to that destination should come down.