Skip to main content

What is idempotency?

Idempotency means an operation produces the same result whether it runs once or many times. For webhooks, an idempotent handler can receive the same event twice, or five times, without side effects piling up: one refund stays one refund, one welcome email stays one welcome email. It is the receiver-side answer to the fact that webhook delivery is at-least-once, so duplicates are a matter of when, not if.

Sending webhooks?
Svix is the enterprise-ready webhook sending service. It handles signing, retries, and delivery observability, so you can ship a reliable webhook platform in minutes instead of months. Start sending webhooks with Svix.

Why duplicates are normal

A provider sends a webhook and waits for a 2xx. When the response doesn't arrive in time, the provider cannot tell whether the request was lost on the way in or the response was lost on the way back. In the second case your handler already ran. The only safe move is to retry, which means the same event arrives again at a handler that already processed it.

Nothing exotic has to go wrong for this to happen: a deploy that restarts your server mid-request, a proxy returning 502 for a minute, a handler that takes nine seconds against a five-second timeout. Every one of these produces a duplicate delivery with a perfectly valid signature. Duplicates also arrive through replay attacks and through manual replays after an outage, which is why deduplication shows up in security guidance as well as reliability guidance.

Deduplicating on the message ID

The standard mechanism is an idempotency key: a unique ID the provider attaches to each message, stable across every retry of that message. Providers following the Standard Webhooks specification send it in the webhook-id header, and the spec recommends receivers use it to drop messages they have already processed.

The receiving pattern is short: after verifying the signature, check whether the ID is in your store of processed messages. If it is, return 2xx without doing anything, because telling the provider "already handled" and "handled just now" is the same thing. If it isn't, record the ID and process the event.

Two details matter in practice.

  • Record the ID and process atomically, or record first. If you process first and crash before recording, the retry runs the side effect again. A unique-constraint insert on the ID inside the same transaction as your side effect is the clean version.
  • A short TTL is enough. Retries and replays cluster within hours, and anything older than the timestamp tolerance window fails verification anyway. A cache or table with a TTL of a few days covers realistic retry schedules without growing forever.

Naturally idempotent handlers

Some handlers don't need a deduplication store because the operation itself is idempotent. Setting subscription.status = "canceled" lands on the same state no matter how many times it runs. Upserting a record keyed on an ID from the payload is likewise safe. When your handler is a pure state-set or an upsert, duplicates are harmless by construction.

The trouble is that handlers grow. The upsert gains a "send notification email" line, and the handler is no longer idempotent even though nothing about its shape announces that. Explicit deduplication on the message ID keeps working when the handler changes, which is why it is worth having even when today's logic looks safe.

What idempotency does not fix

Deduplication drops repeats of the same message. It does not fix ordering: a retried user.updated can arrive after a newer user.updated, and both have distinct IDs, so both process. Handlers that care should compare a timestamp or version from the payload and skip stale updates. It also does not remove the need for exactly-once thinking elsewhere; idempotent processing is precisely how you get exactly-once effects out of at-least-once delivery.

For the full receiving-side treatment, see the idempotency and deduplication lesson in Webhooks University.

Frequently asked questions

What is an idempotency key in webhooks?

A unique ID the provider attaches to each message, stable across every retry of that message. Under the Standard Webhooks specification it is the webhook-id header. Receivers store processed IDs and return 2xx without reprocessing when an ID repeats.

Why do webhooks arrive more than once?

Because delivery is at-least-once. When a provider does not receive a timely 2xx it cannot tell a lost request from a lost response, so it retries. If the response was the thing that got lost, the handler already ran and the retry is a duplicate.

How long should I store processed webhook IDs?

Long enough to cover the provider's retry schedule, typically a few days. Duplicates cluster within hours of the original delivery, and messages older than the timestamp tolerance window are rejected during verification anyway, so the store can expire entries aggressively.

Does idempotency guarantee events are processed in order?

No. Deduplication drops repeats of the same message, but two different messages can still arrive out of order after a retry. Handlers that depend on order should compare a timestamp or version number in the payload and ignore stale updates.

Ready to send webhooks?

Svix handles signing, retries, rate limiting, and delivery observability for the webhooks you send to your users, so your team can stay focused on your product.