# Retries and replay

Webhook retries with exponential backoff, the per-destination circuit breaker, the dead-letter queue, and replaying one event or a whole backlog.

Source: https://docs.webhooker.eu/deliver/retries-and-replay/

## What counts as a failure

| Response | Outcome |
| --- | --- |
| `2xx` | Success. |
| `408`, `429`, any `5xx` | Retryable. |
| Any other `4xx` | Permanent failure — status `failed`, no retries. |
| Timeout, connection error, DNS failure | Retryable. |
| `1xx`, `3xx` | Retryable. The endpoint is misbehaving, not refusing. |

The distinction matters: a `404` from your endpoint means the route does not
exist, and retrying for five hours will not create it. A `500` means your app
broke on a request it accepted, so retrying is exactly right.

Return a `2xx` as soon as the payload is safely on your side and do the work
afterwards. A handler that finishes the job before answering will eventually hit
the timeout, and a timeout is a failure.

## The retry schedule

The default schedule is `30s, 2m, 10m, 1h, 4h`: five retries after the first
attempt, covering about five and a half hours. Each delay carries ±20% jitter so
that a destination coming back up is not hit by every queued retry at once.

The schedule is per destination and can be changed through the
[API](/reference/api/#destinations) — up to 10 intervals, each between 1 second
and 24 hours. A short schedule fails fast; a long one holds a backlog longer.
[How exponential backoff is tuned in practice](https://webhooker.eu/blog/webhook-retries-exponential-backoff)
covers what the providers themselves do, if you are picking your own intervals.

When the last interval has been tried and failed, the delivery becomes
`exhausted` and moves to the dead-letter queue.

## The circuit breaker

Five consecutive retryable failures open a destination's circuit. While it is
open, deliveries to that endpoint pause for a cooldown that starts at 60 seconds
and doubles on each re-open, up to 10 minutes. One probe is then let through:

| State | Shown as | Meaning |
| --- | --- | --- |
| `closed` | — | Normal. Everything is being attempted. |
| `open` | **Circuit open** | Cooling down. Deliveries wait. |
| `half_open` | **Recovering** | One probe in flight. Success closes the circuit, failure re-opens it. |

Any response that proves the endpoint is reachable — a `2xx`, or even a `4xx` —
closes the circuit and resets the counter. Nothing is lost while a circuit is
open; deliveries queue and resume.

## Dead letters

The source's **Dead letters** tab lists deliveries that ended as `exhausted` or
`failed`, with the last error and last response status for each, and a summary
per gateway. Filter by `all`, `exhausted` or `failed`.

This is the list to work through after an outage: fix the endpoint, then replay.
A dead-letter queue is what keeps an outage from becoming permanent data loss —
[the reasoning behind it](https://webhooker.eu/blog/webhook-dead-letter-queue-replay)
is worth reading before you design the runbook around it.

![The Dead letters tab of a source](https://docs.webhooker.eu/screenshots/source-dlq-light.webp "Dead letters, grouped by destination. Replay all recreates every exhausted delivery as a fresh pending one.")

## Replaying

**One event.** Open the event and resend it. New deliveries are created and run
the normal retry schedule. Disabled gateways are included — a resend is an
explicit operator action, not automatic traffic.

**A backlog.** Replay in bulk per gateway, optionally narrowed to a status and a
time window. The default targets `exhausted` deliveries, which is the usual "my
endpoint was down for two hours" case:

```bash
curl -X POST https://app.webhooker.eu/api/v1/deliveries/resend-bulk \
  -H "Authorization: Bearer $WEBHOOKER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "connection_id": "…",
        "statuses": ["exhausted", "failed"],
        "since": "2026-09-18T00:00:00Z"
      }'
```

```json
{ "created": 143 }
```

Replay is only possible while the events are still inside your retention window.

:::caution
A replay delivers the payload again. Your handler must be idempotent — key on
`X-Webhooker-Event-Id`, which is stable across attempts and replays. See
[idempotency keys for webhook handlers](https://webhooker.eu/blog/webhook-idempotency-keys)
for the storage patterns that survive concurrent duplicates.
:::
