Retries and idempotency
What a retry preserves, and how a consumer stops processing the same event twice.
In short
- The default schedule is eight attempts over about 21 hours, and it is settable per endpoint.
- A retry keeps the same delivery id, which is what makes it safe to ignore.
- Idempotency has two halves: the producer key and the consumer check.
- 410 Gone stops immediately — that is the consumer unsubscribing, not a failure.
The schedule
| Attempt | Waits before it |
|---|---|
| 1 | none — immediate |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 30 minutes |
| 6 | 2 hours |
| 7 | 6 hours |
| 8 | 12 hours |
Roughly 21 hours end to end, which crosses a deploy, a certificate renewal and most incidents. A destination that needs something else gets its own schedule:
curl -X PATCH https://api.hookget.com/v1/endpoints/ep_… \
-H "authorization: Bearer $HOOKGET_KEY" \
-d '{"retry_schedule":[0,10,60,300],"max_attempts":4}'
Idempotency, both halves
Producer side. Send an idempotency key with a publish and the same key never produces a second event, however many times the request is retried by your own infrastructure.
-d '{"type":"order.created","payload":{…},"idempotency_key":"ord_10241-created"}'
Consumer side. webhook-id is constant across every retry of a
delivery. Store it and ignore anything you have already handled:
const id = headers['webhook-id'];
if (await seen(id)) return res.status(200).end(); // already handled
await handle(event);
await remember(id); // then remember it
res.status(200).end();
Acknowledge fast and work afterwards. A consumer that does thirty seconds of processing before answering will be retried while it is still working, and then has to be idempotent about its own half-finished work.
What stops the schedule early
| Response | What happens |
|---|---|
| 2xx | Delivered. The attempt is recorded and the failure counter resets |
| 410 Gone | The destination is disabled immediately — it has removed itself |
| 4xx (other) | Retried: a 401 is usually a rotated secret, not a permanent refusal |
| 5xx / timeout / connection error | Retried on the schedule |
| Schedule exhausted | Moved to the dead-letter queue and kept |