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 200 to 299, 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:

AttemptWait before the next attempt
15 seconds
215 seconds
345 seconds
4About 2 minutes
5About 7 minutes
6About 20 minutes
7About 1 hour
8About 3 hours
96 hours
10Final 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-id and the request body are identical on every attempt of one event.
  • svix-timestamp and svix-signature are 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:

TypeScript
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 2xx for 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 2xx for events you do not care about. A 4xx for 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.