Retries, duplicates and idempotency
July 23, 2026 · 4 min read
Webhook delivery is “at least once”, never “exactly once”. That is not a bug – it is the only honest guarantee a network can give. Which means: sooner or later the same event will arrive twice, and your code has to be ready.
Why the same event arrives twice
The provider sends, your server processes the order and answers 200 – but the response gets lost on the way back. From the provider's view the delivery failed, so it retries. Your side now processes the same order a second time. Nobody did anything wrong; the network simply cannot promise more.
How providers retry
Typically with exponential backoff: after seconds, then minutes, then hours – Stripe keeps trying for up to three days, GitHub gives up much sooner, and many providers disable an endpoint that fails for long enough. Two consequences follow: a brief outage on your side is usually harmless, and a permanently broken endpoint quietly stops receiving events.
The idempotency pattern
Idempotent means: processing the same event twice has the same effect as processing it once. The recipe is always the same – take the event ID from the payload, remember it, skip anything you've seen before:
async function handleWebhook(event) {
const id = event.id; // provider's unique event ID
if (await seen.has(id)) return 200; // already processed → done
await seen.add(id, { ttl: "7d" }); // remember before working
await doTheWork(event); // charge, email, provision …
return 200;
}
Store the IDs wherever you already keep state – a database table with a unique index, Redis with a TTL. The unique index is the important bit: it makes the check atomic, so two simultaneous deliveries can't both pass.
What if there's no event ID?
Build one from stable fields: order_id + event_type + timestamp, hashed. Anything that identifies the event and doesn't change between retries works.
Answer fast, work later
The single most effective measure against retries is a quick 2xx. Validate, put the job on a queue, answer immediately – then do the slow work. A webhook handler that sends emails synchronously will eventually hit a timeout, get retried and send the email twice.
When duplicates don't matter
Be pragmatic: if the webhook only triggers a push notification, a duplicate means you hear a sound twice – annoying, not expensive. Effort should match consequences: full idempotency for payments and provisioning, relaxed handling for pure notifications.
More: which status code to return · best practices
Get Webhooky
Free for your first 100 notifications – set up your endpoint in two minutes.