Build an idempotent event log
Record webhook events once, route workers safely, and make duplicate delivery harmless.
Webhook delivery is at least once. Your event log is what turns duplicate delivery, replay, retries, and worker crashes into safe operational behavior.
Dedupe before side effects
Insert the event id into a unique log before fulfillment. If the insert says the event already exists, acknowledge the webhook and do not run side effects again.
Event log shape
| Field | Purpose |
|---|---|
event_id | Unique event identifier and dedupe key. |
event_type | Routes the event to the correct worker. |
received_at | Supports debugging and latency monitoring. |
processed_at | Shows when business logic completed. |
processing_state | Pending, processing, succeeded, ignored, or failed. |
related_reference | Session id, rail id, quote id, customer id, or your local attempt id. |
Insert-once flow
Implementation pattern
async function recordVerifiedEvent(event: ObitWebhookEvent) {
const inserted = await db.webhookEvents.insertIgnore({
eventId: event.id,
eventType: event.type,
relatedReference: event.data.id,
processingState: 'pending',
receivedAt: new Date(),
});
if (!inserted) {
return { duplicate: true };
}
await jobs.enqueue('process_obit_event', { eventId: event.id });
return { duplicate: false };
}
async function processRecordedEvent(eventId: string) {
const event = await db.webhookEvents.claimForProcessing(eventId);
if (!event) return;
try {
await applyBusinessTransition(event);
await db.webhookEvents.markSucceeded(eventId);
} catch (error) {
await db.webhookEvents.markFailed(eventId, redactError(error));
throw error;
}
}State transitions
| Current state | Next state | Trigger |
|---|---|---|
| Pending | Processing | Worker claims the event. |
| Processing | Succeeded | Business transition finishes. |
| Processing | Failed | Worker error after safe logging. |
| Failed | Processing | Operator or scheduled retry. |
| Any terminal | Same terminal | Duplicate event or replay. |
Guardrails
- Put a unique constraint on event id.
- Keep raw payload access restricted and redact it from logs.
- Store enough identifiers for support without storing secrets.
- Make business transitions idempotent too, not just event insertion.
- Route unknown event types to an ignored or review state, not a crash loop.