Webhook Retries and Idempotency
Build idempotent webhook handlers for email delivery events, bounces, complaints, and inbound email.
TL;DR
Webhook providers retry because networks fail. Store event IDs, process each event once, return 2xx only after durable work, and make state transitions monotonic so duplicate or out-of-order events do not corrupt email status.
What you will learn
- Make webhook handlers safe under retries
- Store event IDs and apply monotonic state transitions
- Avoid losing events during deploys or outages
Why retries exist
Webhooks cross the public internet. Timeouts, deploys, load balancers, and database outages happen. A provider cannot know whether your application processed an event unless your endpoint returns a successful response.
That means duplicate delivery is a feature, not a bug.
Store before side effects
The reliable pattern is:
- Verify the signature.
- Parse the event.
- Insert the event ID into a table with a unique constraint.
- If the insert conflicts, return 200.
- Apply side effects.
- Mark processing complete.
- Return 200.
For high-throughput systems, enqueue after the unique insert and process asynchronously.
Example schema
create table webhook_events (
provider text not null,
event_id text not null,
event_type text not null,
received_at timestamptz not null default now(),
processed_at timestamptz,
payload jsonb not null,
primary key (provider, event_id)
);
This lets retries hit the same primary key instead of running the side effect twice.
Monotonic email status
Email events can arrive out of order. For example, you may see delivered after a temporary deferred, or an old accepted event after a final bounced event.
Define status precedence:
| Status | Rank |
|---|---|
| accepted | 10 |
| deferred | 20 |
| delivered | 30 |
| bounced | 40 |
| complained | 50 |
Only move the stored message state forward unless the new event is a correction you explicitly support.
Response codes
Return:
2xxafter durable acceptance.4xxonly when the request is invalid and should not be retried.5xxwhen retrying later may succeed.
Do not return 500 after a duplicate event has already been processed. That creates unnecessary retry noise.
Operational checklist
- Verify signatures from the raw body.
- Record event IDs with a unique key.
- Use database transactions for status updates.
- Make downstream notifications idempotent too.
- Monitor webhook failure rates.
- Keep old and new provider handlers during migration.
- Reconcile daily counts with provider dashboards.
Pair this with webhook signature verification before processing production traffic.
References
Frequently asked questions
- Can a webhook arrive more than once?
- Yes. Duplicate delivery is normal. Your handler must be idempotent.
- Should I return 200 before processing?
- Only if the event has been durably stored or enqueued. Otherwise a crash can lose the event.
Related guides
Put the guide into production
Postscale brings sending, inbound processing, DMARC reporting, and masked addresses behind one API so the operational pieces stay connected.