At-least-once delivery means some webhooks may arrive twice. Learn why idempotency on the receiver is non-negotiable, how exponential backoff works, when a delivery ends up in the DLQ, and how to replay safely without duplicating side effects.
When a webhook delivery fails — a timeout, a 5xx error, a network blip — the platform retries. The guarantee is that your endpoint will receive the event at least one time. Not at most once, and not exactly once. At least once.
You will not lose an event because your endpoint was temporarily unavailable or slow. If a delivery attempt hangs, times out, or fails with a 5xx error, Centrali retries with exponential backoff until the event succeeds or the retry window expires.
Your endpoint may receive the same event twice. Your code must be idempotent — if the same webhook arrives twice (because of a retry after your endpoint processed it, or a manual replay), you must not create two records or charge twice.
The same webhook will arrive at least once. Your handler must recognize when it has already processed an event and return success without repeating the side effect. Here's the pattern:
// Node.js / Express handlerapp.post('/webhooks/centrali', async (req, res) => {const event = req.body;// 1. Use the event ID as an idempotency keyconst existing = await db.query('SELECT id FROM events_processed WHERE event_id = ?',[event.recordId] // Centrali includes recordId in the payload);if (existing.length > 0) {// Already processed — return successreturn res.status(200).json({ ok: true, duplicate: true });}// 2. Process the eventif (event.event === 'record_created') {await db.query('INSERT INTO user_records (id, data) VALUES (?, ?)',[event.recordId, JSON.stringify(event.data)]);}// 3. Mark it processed atomicallyawait db.query('INSERT INTO events_processed (event_id, processed_at) VALUES (?, NOW())',[event.recordId]);return res.status(200).json({ ok: true });});
Why check-then-process isn't enough: Between the time you check for an existing record and the time you insert, another copy of the same webhook may arrive. Use a database unique constraint on the event ID, or wrap both operations in a transaction. Many teams get this wrong and silently create duplicate records on retries.
Retrying immediately on failure is wasteful. If an endpoint is overloaded, hammering it faster makes it worse. Backoff spreads the retries over time, giving the endpoint a chance to recover.
You can customize this schedule per subscription — set attempt count (1–20), choose exponential or fixed backoff, and set a retry window (up to 7 days). For most use cases, the default 5 retries over ~40 minutes works well.
Design for it anyway: no delivery platform is immune to an outage, ours included. Treat missing webhooks as a normal condition rather than an exception — reconcile against the stored event record when a delivery window looks quiet, and keep your handler idempotent so a catch-up burst is safe.
A delivery lands in the DLQ when all retries are exhausted or the retry window expires. You can inspect, replay, or discard DLQ deliveries — no need to delete the subscription and set it up again.
Access the DLQ via the REST API or the console. Each delivery shows the reason it dead-lettered, the last error, and the stored payload.
// List dead-lettered deliveriescurl -X GET https://api.centrali.io/data/workspace/{workspace}/api/v1/webhook-subscriptions/deliveries/dlq \-H "Authorization: Bearer $CENTRALI_TOKEN"// Response includes:// {// "data": [// {// "id": "dlv_...",// "dlqReason": "max_attempts",// "lastError": "Connection timeout",// "requestPayload": { ... },// "responseBody": "Error message...",// "attemptCount": 5,// "retryWindowMs": 604800000// }// ]// }// Replay a DLQ deliverycurl -X POST https://api.centrali.io/data/workspace/{workspace}/api/v1/webhook-subscriptions/deliveries/{id}/retry \-H "Authorization: Bearer $CENTRALI_TOKEN"// This creates a new delivery attempt with the same payload.// The replay increments the attempt counter fresh, so the retry schedule starts over.
No automatic alerting (yet): Centrali does not send you a notification when a delivery lands in the DLQ. Build a saved query that counts DLQ deliveries per subscription and set up a query alert, or poll the DLQ endpoint on a schedule.
Replay is powerful but requires care. Your endpoint will receive the same payload and signature twice (or more). Here's how to handle it safely.
Make your handler idempotent using the event ID as a key. This is the right default because it works for both retries and manual replays.
// Upsert using event ID as the idempotency keyasync function handleWebhook(event) {// The event.recordId is globally unique and stable across retries/replaysconst result = await db.query(`INSERT INTO webhooks_received (event_id, event_data, received_at)VALUES (?, ?, NOW())ON CONFLICT(event_id) DO UPDATE SETlast_seen_at = NOW()`,[event.recordId, JSON.stringify(event)]);// Trigger downstream work only on first receiptif (result.affectedRows === 1) {await processNewEvent(event);}return { ok: true };}
If your endpoint has been crashing or timing out, wait until you've deployed a fix before replaying. Replaying into a broken endpoint lands it back in the DLQ. Fix the bug, deploy, verify with a test, then replay.
For time-sensitive events (notifications, alerts), discarding an old DLQ delivery may be fine. Discard does not delete it — it marks it as intentionally skipped and removed from the DLQ.
// Discard a DLQ delivery (mark as handled without replaying)curl -X POST https://api.centrali.io/data/workspace/{workspace}/api/v1/webhook-subscriptions/deliveries/{id}/discard \-H "Authorization: Bearer $CENTRALI_TOKEN"
Understand HMAC signing, secret rotation, and how to verify incoming webhooks.
Cross-provider signature verification patterns and provider-specific schemes.
Why storing webhook events as queryable records outlasts provider retention windows.
Compare Centrali, Svix, Hookdeck, Inngest, and roll-your-own approaches.
Side-by-side comparison of Centrali and Svix webhook capabilities.
See how Centrali stacks up against Hookdeck for webhook delivery.
Inspect every delivery, see retry attempts, handle DLQ events, and replay with confidence. Build webhook endpoints that handle retries and duplicates gracefully.
Free tier includes webhook delivery logs and replay