Platform

How are failed webhooks retried, and do events arrive in order?

Two questions about webhook delivery come up together, because a reader who has just found duplicates in their log usually finds them out of order as well. The retry behaviour is precise and published; the ordering has one rule that is easy to state and easy to get wrong.

What counts as a failed delivery?

Less than people expect, and the answer is not about the status code you choose.

Your endpoint has five seconds to respond. Any 2xx counts as success. Everything else counts as a failure, and that includes a 3xx redirect, every 4xx, every 5xx, and a timeout.

What the status code does change is what you see in the delivery attempts log. What it does not change is whether we retry: there is no status code that stops delivery early. So returning a 400 for an event you cannot process does not discard it, and returning 2xx for an event you failed to handle discards it permanently. The code is a note to yourself; the class is the signal.

That five-second budget is the reason to acknowledge before you work. Queue the event, return 200, and do the processing afterwards.

What is the retry schedule?

Fixed and published, which is worth saying because the question is usually asked as "what is your exponential backoff". Ours is not computed from a formula; it is a table.

RetryDelay after the previous attempt
15 seconds
25 minutes
330 minutes
42 hours
55 hours
610 hours
710 hours

That is eight attempts including the first, spread over roughly 27.5 hours. Each delay carries plus or minus 20 percent jitter so that retries to the same endpoint do not synchronise after an outage.

Two responses change the timing rather than the schedule. A 429 or a connection timeout waits at least 60 seconds before the next attempt whatever the table says, and a Retry-After header on your response is honoured.

Every attempt carries the same webhook-id, which is what makes deduplication possible and is covered in handling duplicate webhooks.

What happens after the last retry?

The delivery is permanently failed, and replay is how you get it back.

You trigger one with POST /v1/webhooks/{webhook_id}/replay, or from the endpoint's page in the dashboard. It queues redelivery of events that endpoint missed, both deliveries that failed and events never attempted, for example while it was paused, and returns 202 because the redelivery happens asynchronously. Three properties matter when you reach for it:

  • It skips what already succeeded, so a replay never double-delivers an event the endpoint has already accepted.
  • A redelivered event keeps its original webhook-id, so the deduplication you built for retries covers replays without any extra work.
  • It defaults to the last 24 hours. Pass since and until to bound the window explicitly, which you need if the failures started more than a day ago.

There is a quota: 20 replays per organization per UTC day, and past it the request returns 429 with WebhookReplayQuotaExceeded. So replay is a recovery tool rather than a routine part of a pipeline.

To see what happened rather than guess, read the endpoint's delivery attempts. Each HTTP request has its own entry, newest first, with status codes and latency, so a retried event appears once per attempt.

What if my endpoint keeps failing?

It gets marked, then eventually switched off.

An endpoint's status is active, degraded or paused. Recent failures mark it degraded, which is a warning rather than a change in behaviour: delivery and retries continue. An endpoint that fails continuously for about five days is paused, and then all delivery stops. One successful delivery during that period resets the clock.

The part to plan for is that a paused endpoint never resumes on its own. You re-enable it, from the dashboard or with a PATCH setting its status back to active, and then replay to recover what it missed while it was off.

Four things clear a degraded endpoint back to active: a delivery succeeding, changing its url, re-enabling it from paused, or a test send that returns 2xx. Editing its description or its subscribed event types says nothing about whether it is reachable, so neither clears the flag, and nor does a test send that fails.

Bird emails the organization's owners when an endpoint first becomes degraded, once per episode rather than once per failed delivery, and again if it degrades after recovering.

Is there a dead-letter queue?

Not as a queue you can read, and the difference is worth understanding before you go looking for one.

What people mean by the question is usually a place where messages a consumer could not process are parked, so you can inspect them and retry them later. Bird's answer is shaped differently: the events are not moved somewhere else, the endpoint is taken out of service. A delivery that exhausts its retries is marked permanently failed and stays associated with that endpoint, and an endpoint that keeps failing is paused rather than allowed to keep losing events.

So the three things you would use a dead-letter queue for map onto three different mechanisms:

What you wantWhere it is
See what failed and whyThe endpoint's delivery attempts, one entry per HTTP request with status and latency
Stop losing events while you fix itAuto-pause, which halts delivery rather than draining into a queue
Retry what you missedReplay, bounded by since and until

The practical consequence is that you do not drain anything. You fix the endpoint, re-enable it if it paused, and replay the window in which the failures happened. Nothing accumulates that you have to clear afterwards, and nothing needs consuming on a schedule.

The one thing to watch, since it has no dead-letter equivalent, is the replay quota. Twenty per organization per UTC day is generous for an incident and thin as a routine, which is another way of saying this is a recovery path rather than a second delivery channel.

Do events arrive in order?

No, and nothing in the system promises they will.

An email.delivered can arrive before the email.accepted for the same message, and retries make it more likely. The rule is one line: sort by the timestamp field inside the event payload, never by arrival order.

The consequence is bigger than sorting a list, because a handler that overwrites state on each event can be corrupted by a late arrival. Bird's own SMS guidance is the worked example. A message's cost has to be merged one component at a time, keeping for each component the value from the event with the latest timestamp, rather than replacing the whole object. The amount in any one payload sums only the components in that payload, so it reads as the charge so far and not as a settled total. Replace the object wholesale and an older event can undo a newer charge.

The same shape applies to anything you accumulate from events: merge per field, keep the newest timestamp per field, and treat any single payload as partial.

Webhooks has the delivery semantics in full, and SMS events has the cost merge in its own context.

Bouw op hetzelfde netwerk.

Een test-API-key is direct beschikbaar. Productietoegang wordt ontgrendeld zodra u een betaalmethode toevoegt en een afzender verifieert.

Begin met één kanaal.
Voeg de rest toe wanneer je er klaar voor bent.

Een test-API-key is direct beschikbaar. Productietoegang wordt ontgrendeld zodra je een betaalmethode toevoegt en een afzender verifieert.

Gebruik je Claude Code, Cursor of Codex? Kopieer een setup-prompt en je agent installeert de Bird CLI en skills voor je. Kies de jouwe:

Cursor