Writing Idempotent Webhooks: Patterns That Survive Retries
Description and use
Webhook events are often retried, causing duplicate actions. By using a dedupe table, state readback, and fast 2xx responses, processing becomes idempotent and double charges are avoided.
Problem
Webhook providers often retry events, causing duplicate processing and potential double charges or state corruption.
Outcome
Duplicate webhook deliveries are now harmless; orders are only charged once, state transitions are atomic, and retries are suppressed by fast 2xx responses.
Workflow
- Validate signature using raw body HMAC
- Attempt to insert event ID into processed_events table with ON CONFLICT DO NOTHING
- If insert succeeds, enqueue raw payload and return 200 quickly
- If insert fails, read current business state (e.g., order status)
- If state already reflects desired outcome, return 200 without side effects
- Otherwise perform conditional update to transition state only if still pending
- Handle side effects (charge, email, etc.) after state transition
- If worker crashes, queue redelivers and readback guards against repeat work
Evidence
Every webhook arrives twice eventually, so design for it from day one
A unique dedupe key plus a processed-events table kills double-acts
Read current state before acting so a retry is a no-op
Return 2xx fast, do the slow work after you confirm it hasn't run
- Record date: 2026-10-12T09:55:52.946Z