Skip to main content

Delivery & Retry

The dispatcher operates with an at-least-once policy. Network failures and slow merchant responses can cause the same event to arrive more than once. This page covers the retry policy and idempotency patterns for the merchant side.

Retry policy

The dispatcher automatically retries when the response does not count as a success or when a timeout occurs.

FieldValue
Success criteriaHTTP 200–299, or 409 Conflict (idempotent ack)
Response timeout10s (connect 3s)
Backoff formuladelay = min(1s × 2^retry_count, 1024s)
Max retries10
Total cumulative wait~1023s (~17 minutes)
Respond within 10s

Offload heavy work to a queue (e.g. Sidekiq, Asynq) and return 200 immediately. Anything over 10s triggers a retry.

Backoff schedule

Attemptretry_countdelay (s)cumulative (s)
100
2011
3123
4247
53815
641631
753263
8664127
97128255
108256511
1195121023

Response code semantics

Status codeDispatcher interpretation
200–299Successfully received. Logged as DELIVERED in webhook_log.
409 ConflictIdempotent ack — interpreted as "already received". Marked DELIVERED, no retry.
Other 4xxTriggers retry (redeliver after backoff).
5xx / timeoutTriggers retry (redeliver after backoff).
Response body is not inspected

The dispatcher determines success or failure based on the HTTP status code alone. The merchant response body is stored in webhook_log up to 1KB and used only for debugging.

Idempotency / duplicate handling

Merchants should use the webhook-id header as a dedup key to safely handle duplicates.

Example dedup table

CREATE TABLE webhook_log (
webhook_id CHAR(36) PRIMARY KEY,
payment_id CHAR(36) NOT NULL,
event_type VARCHAR(64) NOT NULL,
received_at TIMESTAMPTZ DEFAULT NOW(),
INDEX (payment_id, received_at)
);

Handling pattern

1. Verify signature (fail -> 401)
2. INSERT INTO webhook_log (...) ON CONFLICT (webhook_id) DO NOTHING
-> if 0 rows affected this is a duplicate; respond 200 OK (or 409 Conflict) immediately
3. Domain processing (inside a transaction)
4. 200 OK
409 is safe for duplicates

If the merchant responds with HTTP 409 Conflict, the dispatcher interprets it as an "already received" signal and marks DELIVERED without retrying. Responding 409 instead of 200 for the duplicate-dedup case is safe, and it actually makes the audit log clearer by distinguishing "idempotent attempts" from "newly processed attempts" via the response code. Other 4xx/5xx codes trigger retries, so use them carefully.

Transaction pattern example

import { pool } from './db.js';

async function processWebhook(req, res) {
// 1. Assume signature verification has already passed in an earlier step.
const event = JSON.parse(req.body.toString('utf8'));
const webhookId = req.get('webhook-id');

const client = await pool.connect();
try {
await client.query('BEGIN');

const result = await client.query(
`INSERT INTO webhook_log (webhook_id, payment_id, event_type)
VALUES ($1, $2, $3)
ON CONFLICT (webhook_id) DO NOTHING`,
[webhookId, event.data.payment.id, event.type]
);

if (result.rowCount === 0) {
await client.query('ROLLBACK');
return res.status(409).send('already received'); // idempotent ack
}

// Domain processing
switch (event.type) {
case 'payment.completed': await markOrderPaid(event.data.payment); break;
case 'payment.failed': await markOrderFailed(event.data.payment); break;
case 'payment.expired': await restoreCart(event.data.payment); break;
case 'payment.cancelled': await markOrderCancelled(event.data.payment); break;
}

await client.query('COMMIT');
res.status(200).send('ok');
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
  • Receive rate: webhooks received per unit time
  • Signature verification failure rate: detects abnormal traffic (should converge to 0 in steady state)
  • Duplicate rate: rate of repeated arrivals for the same webhook-id (verify at-least-once is at normal levels)
  • Processing latency (p95): time from receipt to 200 response. If close to 10s, consider introducing a queue.
  • Retry-reach rate: fraction of webhooks not acked on first try

Next steps