Webhooks can fail to deliver for many transient reasons — a receiving server briefly down, network blip, timeout. This guide covers designing a robust retry strategy that handles failures gracefully without overwhelming the receiver.
Why Naive "Retry Immediately Forever" Is a Bad Strategy
Immediately and repeatedly retrying a failed webhook can amplify problems — if the receiver is struggling (perhaps overloaded, which caused the original failure), aggressive immediate retries add more load precisely when it's least helpful, potentially worsening the receiver's situation.
Exponential Backoff: The Standard Approach
function getRetryDelay(attemptNumber) {
const baseDelay = 1000; // 1 second
return baseDelay * Math.pow(2, attemptNumber);
}
// Attempt 1: 1s, Attempt 2: 2s, Attempt 3: 4s, Attempt 4: 8s...
Each retry waits progressively longer — gives a struggling receiver time to recover, and reduces load on both sender and receiver compared to rapid repeated attempts.
Adding Jitter to Avoid Thundering Herd
function getRetryDelay(attemptNumber) {
const baseDelay = 1000 * Math.pow(2, attemptNumber);
const jitter = Math.random() * baseDelay * 0.3;
return baseDelay + jitter;
}
If many webhook deliveries fail simultaneously (a receiver outage affecting many events at once), pure exponential backoff without jitter means many retries arrive at exactly the same moment when backoff periods align — adding randomized jitter spreads retry attempts out, avoiding this "thundering herd" pattern.
Setting a Maximum Retry Count/Duration
const MAX_RETRIES = 5;
const MAX_RETRY_WINDOW = 24 * 60 * 60 * 1000; // 24 hours
Retries shouldn't continue indefinitely — define both a maximum retry count and/or a maximum total time window, after which the delivery is considered permanently failed and requires different handling.
Distinguishing Retryable from Non-Retryable Failures
| Response | Retry? |
|---|---|
| 5xx server error, timeout, connection failure | Yes — likely transient |
| 4xx client error (bad request, unauthorized) | Generally no — retrying an inherently invalid request won't succeed |
Storing Failed Webhooks for Later Investigation
See How to Set Up Dead Letter Queues for Failed Messages — after exhausting retries, don't simply discard the failed webhook silently; store it (a "dead letter" record) for investigation and potential manual redelivery.
Providing Webhook Delivery Status to the Receiver
If you're building a platform that sends webhooks to third parties, consider providing a dashboard/API where they can see delivery status/history and manually trigger redelivery — genuinely useful for debugging integration issues on their end.
Making Your Own Webhook Endpoints Idempotent (Receiving Side)
See How to Implement Idempotent API Endpoints — since retries can result in duplicate delivery (a webhook that "failed" from the sender's perspective due to a timeout might have actually been processed by your receiving endpoint), design your webhook receiver to handle duplicate delivery gracefully.
Documenting Your Retry Behavior for Webhook Consumers
See How to Design and Secure Webhook Endpoints — if you're the sender, document your retry behavior (schedule, max attempts, timeout) clearly for whoever's receiving your webhooks, so they can build appropriately resilient handling on their end.
Common Errors
Retries causing duplicate processing on the receiving end — ensure webhook payloads include a unique event ID, and receivers deduplicate based on this ID rather than assuming each delivery represents a genuinely distinct event.
Continue Reading
- How to Design and Secure Webhook Endpoints
- How to Implement Idempotent API Endpoints
- How to Set Up Dead Letter Queues for Failed Messages
Browse more articles in Object Storage, Messaging & APIs.