Webhook Retry Strategy: How to Avoid Duplicate Events

Devicode Team
2 min read

Webhook retries are useful only when your consumer can safely receive the same event more than once. That is the real delivery problem. Networks fail, providers time out, and your own API can return a 500 after it already processed the request. If your retry plan does not include idempotency, you will eventually create duplicate records.

What a retry strategy needs

A usable webhook strategy has four pieces:

  • a durable event ID from the sender
  • an idempotency key or dedupe table on the consumer
  • retry logic with backoff and a capped retry window
  • clear logging for success, failure, and replayed deliveries

Without all four, retries become guesswork.

Treat the event as the unit of truth

Do not key your database writes only on arrival time or request order. Use the sender’s event ID and store the processing result once. If the same event arrives again, return success after confirming the original processing already happened.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
app.post('/webhooks', async (req, res) => {
  const eventId = req.header('X-Event-Id');

  if (await alreadyProcessed(eventId)) {
    return res.status(200).send('duplicate ignored');
  }

  await markProcessed(eventId);
  await handleEvent(req.body);

  return res.status(200).send('ok');
});

That pattern is boring on purpose. Boring is what you want when retries can repeat a payment, an email, or a lifecycle event.

Exponential backoff: the standard approach

Retrying immediately after a failure usually hits the same congestion or error. Exponential backoff spaces attempts out:

1
2
3
function retryDelay(attempt) {
  return Math.min(1000 * Math.pow(2, attempt), 30000); // cap at 30s
}

Pair this with a maximum retry count (typically 5–10 attempts over a few hours) so failed deliveries eventually move to a dead-letter queue rather than retrying indefinitely.

What to retry and what to drop

Retry transient failures: timeouts, 429 responses, and temporary 5xx errors. Do not retry validation failures that will never succeed. If the payload is malformed, the sender should see a clear 4xx response and stop retrying.

Dead-letter queues for undeliverable events

When an event exhausts all retries, move it to a dead-letter queue rather than deleting it. That gives your operations team visibility into what failed and lets you replay individual events after the root cause is fixed.

Why this matters for SaaS teams

Webhook-driven systems often power billing, onboarding, and product analytics. A duplicate event there becomes a duplicate invoice, duplicate signup, or duplicate lifecycle update. The fix is not “retry less.” The fix is “retry safely.”

If you are building delivery infrastructure, SendPromptly is the product to review first.