## TL;DR

Make every handler idempotent anyway, because dedupe tables can be lost and replays can predate them. Reserve 5xx for "I failed, try again" and use it deliberately. 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.

## When to use

You are seeing this: Clerk, via Svix, retries any delivery that does not return 2xx, for up to 3 days. Use this skill when you run into "Clerk webhooks retry for 3 days: dedupe with svix-id and return 200 fast".

## When not to use

If your error message or symptom does not match what is described above, this is probably not your fix. Search for your exact error text instead of forcing this one to fit.

## Versions

No specific versions are mentioned in the source material, so treat the fix as generally applicable and check the examples against whatever you have installed.

## 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.
