Webhook reliability
Webhook Retries and Idempotency: Prevent Duplicate Actions
Make webhook retries safe with event IDs, idempotent receivers, exponential backoff, fast acknowledgments, action logs, and replay controls.
Webhook retries are necessary because networks fail, servers restart, and downstream APIs time out. They are also dangerous when one event can create a second order, send another email, or add the same contact twice.
The receiver must assume at-least-once delivery: accept that a valid event may arrive more than once, give it a stable identity, and make repeated processing produce the same final result.
Make webhook retries safe with idempotency
Retries are safe only when the receiver can recognize an event it has already processed.
A receiver-side flow is:
- Verify the signature and timestamp.
- Read a stable event ID.
- Attempt to reserve that ID in durable storage.
- If it already succeeded, return
2xxwithout repeating the action. - If it is new, enqueue or process the work.
- Record success or a retryable failure.
Core Forms retries failed asynchronous actions after 2, 4, and 8 minutes, then records the final state in its action logs. Your receiving endpoint still needs idempotency because a response can be lost after the action succeeds.
Choose a stable idempotency key
Best options are:
- provider-issued event ID;
- Core Forms submission ID plus action ID and event type;
- immutable business operation ID;
- a canonical payload digest when no ID exists.
Do not use only a timestamp. Two events can share it, and one event can be retried with a new transport timestamp.
A storage record can contain:
event_id
status: processing | succeeded | failed
first_seen_at
last_attempt_at
attempt_count
result_reference
Enforce a unique database constraint on event_id. A check followed by an insert without a constraint can race when two deliveries arrive together.
Separate retryable from permanent failure
Retry these conditions:
- connection timeout;
- temporary DNS or network failure;
- HTTP 408, 429, or most 5xx responses;
- service maintenance;
- database lock or transient queue failure.
Usually do not retry:
- invalid signature;
- malformed required data;
- unsupported event type;
- forbidden operation;
- missing resource that will not be created later;
- validation failure requiring a configuration change.
Record a clear reason. Endless retries hide permanent configuration errors and create noise.
Use exponential backoff with limits
Backoff gives a recovering service room to breathe. Add a maximum attempt count and, for large systems, jitter so many failed events do not retry at the same second.
The sender should record:
- attempt number;
- scheduled time;
- response status;
- bounded response excerpt;
- duration;
- final state;
- manual retry actor and time.
Never log secrets, authorization headers, passwords, or full sensitive payloads.
Acknowledge quickly, process durably
If processing may take several seconds, verify and enqueue the event, then return 2xx. A slow endpoint can complete the action but time out before the sender sees the response, causing a duplicate retry.
The durable queue should own:
- retry scheduling;
- concurrency limits;
- dead-letter or failed state;
- manual replay;
- operational alerts.
Core Forms supports background actions and logs each attempt. The webhooks versus Zapier guide can help decide when direct infrastructure is worth owning.
Make business actions idempotent too
Event deduplication is the first layer. The downstream action should also resist duplicates.
Examples:
- update an order by provider payment ID instead of inserting blindly;
- upsert a CRM contact by stable email or contact ID;
- send a receipt only when
receipt_sent_atis empty; - create a post only when the submission has no linked post ID;
- fulfill a license only when the transaction is paid and unfulfilled.
Use transactions or conditional updates when two workers can process the same operation.
Manual replay needs guardrails
Operators need a retry button, but it should not bypass authenticity or duplicate protection.
Show:
- event and submission references;
- last failure reason;
- attempt history;
- whether the operation already succeeded downstream;
- the effect of replay;
- who initiated it.
Replaying a failed email is different from replaying a payment fulfillment. Put higher-risk actions behind explicit confirmation and audit logs.
FAQ
What is an idempotent webhook?
Processing the same event more than once produces the same final state without repeating side effects.
Why do webhooks get delivered twice?
The sender may not receive the successful response because of a timeout or network failure, so it retries.
Should every failure be retried?
No. Retry temporary failures. Stop and surface permanent validation, authentication, or configuration errors.
Where should I store event IDs?
Use durable storage with a unique constraint and a retention period longer than the sender’s retry window.
Is signature verification enough?
No. It proves authenticity, not uniqueness. Verify signatures and enforce idempotency.