# Surviving retries

Clerk, via Svix, retries any delivery that does not return 2xx, for up to 3 days. Replays from the Dashboard resend events on demand. At-least-once delivery is the contract; exactly-once is your job.

## What to do

1. Read the `svix-id` header on every delivery. Store it in a processed-events table before doing any work. If the id is already there, return 200 immediately and do nothing.
2. Return 200 as fast as possible. Acknowledge first, then do the work: enqueue a job, write to a queue, and let a worker process it. The webhook request is an inbox, not a worker.
3. Make every handler idempotent anyway, because dedupe tables can be lost and replays can predate them. Upserts keyed by Clerk ids, not blind inserts.
4. Reserve 5xx for "I failed, try again" and use it deliberately. A bug that 500s on every retry burns 3 days of retries on a poison event; detect repeated failures and dead-letter them.

## The trap

The slow handler. An endpoint that does 30 seconds of work per event will time out under retry storms, and every timeout becomes another retry. The fix is architectural: the HTTP handler acknowledges, a queue does the work. Also the missing dedupe table: without svix-id tracking, a single replayed `user.created` double-provisions everything downstream.

## Checklist

- The dedupe write must come before the work, and ideally in the same transaction as your first state change.
- Monitor 4xx/5xx rates on the webhook route. A spike means Clerk is retrying; find the poison event in the Dashboard message logs.
- Replays are for testing and recovery. After any incident replay, check the dedupe table absorbed them without double effects.