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.

| Retry | Delay after the previous attempt |
| ----- | -------------------------------- |
| 1     | 5 seconds                        |
| 2     | 5 minutes                        |
| 3     | 30 minutes                       |
| 4     | 2 hours                          |
| 5     | 5 hours                          |
| 6     | 10 hours                         |
| 7     | 10 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](/explained/platform/what-is-idempotency-and-how-do-i-handle-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`](/docs/api/reference/create-webhook-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`](/docs/api/errors/E06010). 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](/docs/api/reference/list-webhook-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 want                       | Where it is                                                                          |
| ----------------------------------- | ------------------------------------------------------------------------------------ |
| See what failed and why             | The endpoint's delivery attempts, one entry per HTTP request with status and latency |
| Stop losing events while you fix it | Auto-pause, which halts delivery rather than draining into a queue                   |
| Retry what you missed               | Replay, 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](/docs/guides/webhooks#delivery-semantics) has the delivery semantics in full, and [SMS events](/docs/guides/sms/events) has the `cost` merge in its own context.