Webhooks
Webhook retries
How Sooner retries failed webhook deliveries with exponential backoff, when it gives up, and how to make your endpoint safe to retry.
If your endpoint does not return a 2xx response, Sooner tries again later. This page lists what counts as a failure, the retry schedule, and what happens when every attempt fails.
What counts as a failure
An attempt fails when any of these happens:
- Your endpoint returns a status outside
200to299, including a redirect (Sooner does not follow redirects). - Your endpoint does not respond within 15 seconds.
- The connection fails, for example a DNS error or a TLS error.
Any 2xx response counts as success, whatever the body. Return it as soon as you have stored the event.
Retry schedule
Sooner makes up to 10 attempts per event for each webhook. After a failed attempt it waits, and the wait triples each time, up to a maximum of 6 hours:
| Attempt | Wait before the next attempt |
|---|---|
| 1 | 5 seconds |
| 2 | 15 seconds |
| 3 | 45 seconds |
| 4 | About 2 minutes |
| 5 | About 7 minutes |
| 6 | About 20 minutes |
| 7 | About 1 hour |
| 8 | About 3 hours |
| 9 | 6 hours |
| 10 | Final attempt |
If every attempt fails, the last retry is made roughly 10 and a half hours after the first. The timing is approximate, and an attempt can start a little later than scheduled.
These timings apply to email events. Contact and domain events use the same attempt limit and growing delays, but their exact timing can differ slightly.
When all attempts fail
After the tenth failed attempt Sooner stops. The event is not delivered to that webhook and is not queued for a later replay. The delivery shows as failed in the dashboard.
Sooner does not disable a webhook that keeps failing. Events keep being attempted, and each one follows the same schedule. If your endpoint will be down for a long time, disable the webhook, and fetch what you missed from the API afterward, for example with GET /emails.
If you delete or disable a webhook while retries are pending, the pending retries are dropped.
What does not change between attempts
svix-idand the request body are identical on every attempt of one event.svix-timestampandsvix-signatureare new on each attempt, because the signature covers the timestamp.
Verify every attempt as usual. See Verify signatures.
Make your endpoint retry-safe
Because the same event can arrive more than once, make processing idempotent:
const processed = new Set<string>(); // use a database table in production
app.post("/webhooks/sooner", express.raw({ type: "application/json" }), async (req, res) => {
const event = wh.verify(req.body, {
"svix-id": req.header("svix-id")!,
"svix-timestamp": req.header("svix-timestamp")!,
"svix-signature": req.header("svix-signature")!,
}) as { type: string; data: Record<string, unknown> };
const id = req.header("svix-id")!;
if (processed.has(id)) return res.sendStatus(200); // already handled, confirm again
processed.add(id);
queue.push(event); // process after responding
res.sendStatus(200);
});- Return
2xxfor events you have already handled, so Sooner stops retrying them. - Return a non-2xx status only when you want Sooner to try again later, for example when your database is unavailable.
- Return
2xxfor events you do not care about. A4xxfor an unknown event type causes pointless retries. - Keep the handler fast. A slow handler that times out is retried even if it eventually finished its work.
See delivery attempts
In the dashboard, open Webhooks and select a webhook to see recent deliveries with their status, event type, number of attempts and last response code. Delivery history is kept for 30 days.
Related
- Webhooks overview for endpoint rules and limits.
- Event types for the payloads.