# Delivery and retries

> How many times we try, what marks an endpoint unhealthy, and how to make your handler safe

Source: https://www.hey.support/docs/developer-guide/webhooks/delivery

---

A delivery succeeds when your endpoint answers **2xx within 10 seconds**. Anything else (a 4xx, a 5xx, a timeout, a connection error) is a failure and is retried.

Acknowledge first and work afterwards: read the body, verify the signature, return `200`, then do the slow part. A handler that calls three other APIs before responding will eventually time out and be retried, which produces duplicates rather than reliability.

## Retries

We attempt each delivery up to **four times**: the first immediately, then after **1s**, **5s** and **30s**. After the fourth failure that delivery is given up on. It stays visible in the delivery log.

## Unhealthy endpoints

After **10 consecutive failures** against the same endpoint we:

1. mark the endpoint **unhealthy**,
2. **disable** it, so we stop sending, and
3. email the workspace owner.

A single success resets the counter, and test deliveries never count toward it. Re-enable the endpoint from **Settings, Developer** once the receiving end is fixed.

## Be idempotent

Delivery is **at-least-once**: a retry after a timeout that actually succeeded on your side, or a resend, means the same event can arrive twice. Key your handler on `event_id`:

```js
async function handle(event) {
  if (await seen(event.event_id)) return;   // already processed
  await process(event);
  await remember(event.event_id);
}
```

`event_id` is stable for one logical event across retries and across endpoints. `id` changes per delivery attempt, so use it for tracing a single attempt in your logs, never for deduplication.

## Ordering

Events are **not** ordered. `message.sent` for a second message can arrive before the first, and `conversation.ended` can beat a `message.sent` that preceded it. Where order matters, use the timestamps inside `data`.

## URL rules

We re-validate the destination on **every** delivery, not only when you save it:

* `https` is required in production.
* Localhost, private ranges, link-local and cloud metadata addresses are rejected.
* Redirects are not followed; a 3xx counts as a failed delivery.

An endpoint that starts resolving to a private address, for instance because a DNS record changed, stops receiving deliveries. To develop locally, use a tunnel that gives you a public `https` URL.

<NextSteps>
  <IconCard icon="key-round" title="Verifying signatures" href="/docs/developer-guide/webhooks/signature" description="Check the HMAC on the raw body before you act." />

  <IconCard icon="webhook" title="Webhooks overview" href="/docs/developer-guide/webhooks" description="The envelope, the headers and every event." />
</NextSteps>
