Back to blog

Webhook Idempotency Explained: Prevent Duplicate Events

Webhook idempotency explained: learn how to prevent duplicate events, handle retries and timeouts, and keep webhooks reliable.

WG

WebhookGuide

April 24, 2026

Introduction

Webhook idempotency means your receiver produces the same final result even if the same event arrives more than once. That matters because most webhook systems use at-least-once delivery, not exactly-once delivery, so duplicates, retries, and delayed deliveries are normal in distributed systems and event-driven architecture.

Providers retry when they do not get a timely 2xx response, so a timeout can turn one delivery into several. A duplicate event can then trigger the same side effect twice unless your handler is built to recognize it. That is why webhook idempotency is a practical safeguard, not just a theoretical concept.

This guide focuses on the failure modes that break real systems: duplicate events, retry behavior, timeouts, and out-of-order delivery. Those problems show up in payments, order processing, subscription updates, and notifications, where one webhook can charge a customer twice, create duplicate orders, or send the same message repeatedly. If you want a deeper background first, see the webhook guide for developers, webhooks explained, and webhook architecture best practices.

The core question is simple: can webhook processing be exactly once, or only effectively once?

What Is Webhook Idempotency?

Idempotency is the property that repeating the same operation does not change the final result after the first successful call. In a REST API, a PUT request can be idempotent because sending the same JSON payload again leaves the resource in the same state; webhook idempotency applies the same idea to inbound events. A webhook may deliver the same Event ID more than once, or send semantically equivalent payloads after a retry, but your handler should apply side effects only once. For a payment confirmation or order_paid event, that means marking the order paid and creating shipment records one time, not on every delivery.

That is different from “ignore duplicates later,” which still lets the first duplicate create damage before you detect it. An idempotent handler prevents the duplicate event from changing state at all, usually by checking the Event ID before any side effect. A webhook handler can be idempotent even when the sender retries; the key is to make the first accepted processing path safe to repeat.

Why Webhook Idempotency Matters

A single Duplicate event can trigger double charges in Stripe, duplicate shipments in Shopify, repeated notifications in Twilio, or inflated orders in your database. It can also corrupt inventory counts and analytics, turning a small delivery glitch into a billing, fulfillment, or reporting incident. That is why webhook best practices for developers and webhook architecture best practices both treat idempotency as core reliability work, not cleanup.

In distributed systems, at-least-once delivery means a Webhook Retry after a timeout, crash, or network split is normal, not exceptional. With webhook idempotency in place, the receiver assumes partial failures and duplicate delivery will happen, then makes processing safe to repeat. That reduces incident severity, speeds recovery after outages, and protects customer trust by keeping billing and operations consistent.

Common Webhook Delivery Problems

Webhook systems usually provide at-least-once delivery, not exactly-once delivery. Providers like Stripe, Shopify, GitHub, and AWS retry when they see a Timeout, a 5xx response, or a transient network failure, which is why webhook delivery retries explained and webhook delivery retry mechanisms matter.

A timeout does not prove the event was not processed. Your receiver may have already created a record, charged a card, or sent an email before the response got lost, then the provider sends the same Duplicate event again.

Out-of-order delivery creates a separate failure mode: a later update can arrive before an earlier one and break state assumptions even if you dedupe correctly. In distributed systems, a Delivery ID may change on each attempt, so dedupe on the provider’s stable event identifier, not the attempt ID.

Idempotency Keys vs Event IDs

An idempotency key is usually sender-generated for an outbound REST API request, such as creating a Stripe charge or posting an order, so the provider can ignore a repeated create call. An Event ID is different: it identifies an inbound Webhook event and should stay stable across retries. A Delivery ID is not the same thing; it often changes on each retry, so it cannot safely dedupe duplicates.

For webhook receivers, prefer the provider’s stable Event ID. Only build a composite dedupe key when the provider does not supply one, using fields like event type, resource ID, and Timestamp: invoice.paid:inv_123:2026-04-24T10:15:00Z. That approach is a fallback, not the default, and it fits the guidance in webhook guide for developers and webhook best practices for developers.

How to Make a Webhook Handler Idempotent

Start with webhook signature verification: validate the provider’s HMAC before you trust the payload. Then use the stable provider-generated Event ID for deduplication, not a request-specific Delivery ID, because delivery IDs often change on each retry.

Store processed Event IDs in a durable system such as PostgreSQL, MySQL, or Redis with an atomic insert-or-ignore step. A UNIQUE constraint plus INSERT ... ON CONFLICT DO NOTHING in PostgreSQL, or INSERT IGNORE / ON DUPLICATE KEY patterns in MySQL, makes duplicate checks an atomic operation instead of a race-prone read-then-write flow. Record the event before or during processing inside a database transaction when possible.

If the insert fails or the ID already exists, short-circuit the request. If it is new, process side effects with retry-safe writes: use upsert, forward-only state transitions, and unique keys for downstream records. For follow-up work, push a job to an Outbox pattern table or a retry queue so the processing path is safe to repeat.

Best Way to Store Processed Webhook Events

The best storage choice depends on how long you need to remember processed events and how costly a duplicate would be.

  • PostgreSQL or MySQL are the best default choices for durable deduplication because they support unique constraints, transactions, and reliable recovery after restarts.
  • Redis is useful when you only need short-lived suppression, such as blocking repeated deliveries for a few minutes during a retry window.
  • For critical workflows like billing, fulfillment, or account state changes, a database is safer than Redis because the dedupe record survives deploys, crashes, and longer retry chains.

A common pattern is a processed_webhooks table with columns like provider, event_id, event_type, received_at, and processed_at. That gives you both deduplication and an audit trail.

Database or Redis for Deduplication?

Use a database when the duplicate would cause a real business problem. A UNIQUE index on provider + event_id lets you claim the event atomically, and the same transaction can update application state.

Use Redis when you need speed and the dedupe window is short. For example, you might store event_id with a TTL to suppress repeated deliveries during a brief retry period. That works for low-risk notifications, but it is not ideal for payments or order state because Redis is not a durable system of record.

In practice, many teams use both: PostgreSQL or MySQL for permanent deduplication, and Redis for temporary rate limiting or replay suppression.

How to Handle Out-of-Order Webhook Events

Out-of-order delivery is common in distributed systems, especially when providers retry one event while newer events continue to flow. The fix is not just deduplication; it is state management.

Use a state machine so each event can only move the object forward through valid transitions. For example, an order might move from pending to paid to fulfilled, but never back to pending.

When the provider includes a Sequence number, use it to ignore older updates. If there is no sequence number, compare the event’s Timestamp and the current object state, but be careful: timestamps are weaker than sequence numbers because clocks and delivery delays can vary.

If an older event arrives after a newer one, store it for audit if needed, but do not let it overwrite the latest state.

What Happens If a Webhook Times Out?

A timeout usually means the provider did not receive a timely response, not necessarily that your handler failed. The provider may retry the same event, which can create a duplicate event if your first attempt actually completed.

That is why the handler should claim the event before doing expensive work, or at least make the work safe to repeat. If the timeout happens after the database commit but before the HTTP response, the provider may retry even though the business action already succeeded.

The safest response is to keep the webhook endpoint fast: verify, dedupe, persist, and hand off long-running work to a background worker. That reduces the chance that a timeout turns into repeated processing.

Should You Verify Webhook Signatures Before Dedupe?

Yes. Verify the Webhook signature verification first, using the provider’s HMAC, before you trust the payload or write dedupe records.

If you dedupe before verifying, an attacker could flood your storage with fake event IDs and create a denial-of-service problem. Signature verification proves the request came from the provider and was not modified in transit, so it should happen before any business logic or persistence.

How to Prevent Race Conditions in Webhook Handlers

Race conditions happen when two deliveries of the same event arrive close together and both pass a naive “check then insert” test.

Prevent that by using an atomic database write:

  1. Start a transaction.
  2. Insert the event ID into a table with a unique constraint.
  3. If the insert fails, treat it as a duplicate and stop.
  4. If it succeeds, continue processing.
  5. Commit after the business update is complete.

This pattern works well with PostgreSQL and MySQL. It is safer than reading first and writing later because the unique constraint becomes the lock.

What Should Happen When a Duplicate Webhook Is Received?

Return a successful response, usually 200, after confirming the event is a duplicate. Do not repeat the side effect.

You should still log the duplicate with the provider name, event ID, and delivery metadata so you can monitor retry behavior and investigate unexpected spikes. If the duplicate arrives because your first attempt timed out, the provider will usually stop retrying once it sees a successful response.

How to Test Webhook Idempotency

Test webhook idempotency the same way you test other failure-prone distributed systems behavior: with retries, concurrency, and malformed delivery patterns.

Good tests include:

  • Send the same event twice and confirm the side effect happens once.
  • Simulate a timeout after the database commit and verify the retry is ignored.
  • Deliver two copies of the same event at the same time to check for race conditions.
  • Send events out of order and confirm the state machine rejects invalid transitions.

If you use Stripe, GitHub, Shopify, Twilio, or AWS webhooks, test with their real retry behavior in a staging environment when possible.

Can Webhook Processing Be Exactly Once?

In practice, no—not across the full path from provider to your system. Network failures, retries, and race conditions make true Exactly-once delivery unrealistic for most webhook integrations, even if systems like Kafka can offer stronger semantics inside a controlled pipeline.

The practical goal is effectively-once processing: verify, dedupe, persist state durably, and make every retry safe to ignore. That is the real answer to webhook idempotency.

Best Practices for Webhook Deduplication

  • Verify signatures before trusting the payload.
  • Use the provider’s stable Event ID rather than a changing Delivery ID.
  • Store dedupe state in a durable database when the action matters.
  • Use a unique constraint and transaction to make the claim atomic.
  • Keep the handler fast and move long-running work to a background worker.
  • Use Redis only for short-lived suppression or rate limiting.
  • Handle out-of-order delivery with a state machine or sequence number.
  • Log duplicates and monitor retry patterns.

Conclusion

Webhook idempotency is what keeps retries, timeouts, and duplicate events from turning into duplicate charges, duplicate orders, or broken state. The safest pattern is simple: verify the signature, claim the event atomically, process once, and make every replay harmless.

That approach works across REST API integrations, JSON payload handling, and event-driven architecture because it treats duplicates as normal behavior in distributed systems rather than as rare exceptions.