Webhook Delivery Guarantees — Complete Guide to Reliability
In this tutorial, you will learn about Webhook Delivery Guarantees. We cover key concepts, practical examples, and best practices to help you master this topic.
Webhook delivery guarantees define at-least-once, exactly-once, and at-most-once semantics, with retry policies, idempotency keys, and dead-letter queues for reliable event delivery to consumers.
What You'll Learn
- Delivery semantics: at-least-once vs exactly-once
- Implementing retry policies with exponential backoff
- Dead-letter queues for failed deliveries
Why It Matters
Without delivery guarantees, consumers cannot rely on webhook data. Missed events cause data inconsistencies, while duplicate events cause processing errors. Clear guarantees set expectations and build trust.
Real-World Use
Durga Antivirus Pro guarantees at-least-once delivery for threat alerts. Each event is retried up to 5 times with exponential backoff (1m, 5m, 15m, 30m, 1h). After 5 failures, the event moves to a dead-letter queue for manual inspection.
flowchart LR
E["Event"] --> Q["Delivery Queue"]
Q --> A["Attempt Delivery"]
A -->|"Success"| D["Delivered"]
A -->|"Fail"| R["Retry Queue"]
R --> A
R -->|"Max Retries"| DLQ["Dead Letter Queue"]
style A fill:#dbeafe,stroke:#2563eb
Code Examples
import time
import requests
from datetime import datetime, timedelta
class WebhookDelivery:
def __init__(self):
self.max_retries = 5
self.backoff_times = [60, 300, 900, 1800, 3600] # seconds
def deliver_with_retry(self, url, event):
for attempt in range(self.max_retries):
try:
resp = requests.post(
url,
json=event,
headers={'Content-Type': 'application/json'},
timeout=10,
)
if resp.status_code in (200, 201, 204):
return True, attempt + 1
if resp.status_code in (400, 422):
# Client error, no point retrying
return False, attempt + 1
except requests.RequestException:
pass
if attempt < self.max_retries - 1:
wait = self.backoff_times[attempt]
print(f"Retry {attempt + 1} in {wait}s")
time.sleep(wait)
return False, self.max_retries
Expected output: Webhook retried with backoff; returns success status and attempt count.
// Idempotency key for exactly-once delivery
const express = require('express');
const app = express();
const processedEvents = new Set();
app.post('/webhook', express.json(), (req, res) => {
const idempotencyKey = req.headers['idempotency-key'] || req.body.id;
if (processedEvents.has(idempotencyKey)) {
return res.status(200).json({ status: 'already_processed' });
}
// Process event
processedEvents.add(idempotencyKey);
console.log('Processing event:', req.body.type);
res.status(200).json({ status: 'processed' });
});
// Clean up old keys periodically
setInterval(() => processedEvents.clear(), 86400000); // 24h
Expected output: Duplicate events with same idempotency key return success without reprocessing.
# Dead letter queue implementation
import json, time
from collections import deque
class DeadLetterQueue:
def __init__(self):
self.queue = deque(maxlen=1000)
def add_failed(self, event, error, attempts):
record = {
'event': event,
'error': str(error),
'attempts': attempts,
'timestamp': time.time(),
}
self.queue.append(record)
self.persist_to_disk(record)
def persist_to_disk(self, record):
with open('dead_letter_queue.jsonl', 'a') as f:
f.write(json.dumps(record) + '\n')
def replay(self, consumer_url):
successes = 0
failures = 0
while self.queue:
record = self.queue.popleft()
try:
resp = requests.post(consumer_url, json=record['event'], timeout=10)
if resp.ok:
successes += 1
else:
failures += 1
except Exception:
failures += 1
return successes, failures
Expected output: Failed events stored in dead letter queue for manual inspection or replay.
Common Mistakes
1. Promising Exactly-Once Without Idempotency
Exactly-once delivery requires consumer-side idempotency. The provider can only guarantee at-least-once at the transport level.
2. Retrying Forever
Without a maximum retry limit, a dead consumer causes infinite retries. Set a reasonable cap.
3. No Dead Letter Queue
Failed events that stop retrying are lost without a DLQ. Always persist undeliverable events.
4. Same Retry Interval
Constant retry interval (every 5s) DDoS the consumer. Use exponential backoff with jitter.
5. Ignoring Consumer 4xx Responses
A 400 response means the consumer cannot Process this event. Do not retry; move to DLQ.
Practice Questions
- What is the difference between at-least-once and exactly-once delivery?
- Why use exponential backoff for webhook retries?
- What is a dead letter queue and why is it needed?
- How does an idempotency key prevent duplicate processing?
- Why should 4xx responses not be retried?
Answers:
- At-least-once guarantees delivery with possible duplicates; exactly-once prevents duplicates via idempotency.
- Backoff prevents overwhelming the consumer; each retry waits longer, giving recovery time.
- A DLQ stores events that failed after max retries, preserving them for manual inspection and replay.
- The consumer stores processed keys and skips duplicates, ensuring exactly-once processing.
- 4xx indicates the consumer rejects the event (bad data, invalid format); retrying will not help.
Challenge: Implement a webhook delivery system with: 5 retry attempts with exponential backoff (1min, 5min, 15min, 30min, 1hr), a persistent dead-letter queue, and a replay endpoint for DLQ events.
FAQ
Mini Project
Build a webhook delivery engine with: configurable retry policy (max retries, backoff schedule), idempotency support via event IDs, dead-letter queue with persistent storage, delivery metrics (success rate, avg retries, DLQ count), and manual replay.
What's Next
Learn about Webhook security for authenticating deliveries, or explore Webhook dead letter queues for handling persistent delivery failures.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro