Idempotency, or You Will Charge Twice
Idempotency is the property that repeating the same operation produces the same durable effect as doing it once. In payment systems it usually means attaching a stable key to an event or request, then using atomic database constraints so retries, duplicate webhooks, and concurrent handlers cannot create duplicate charges, credits, shipments, or records.
The need comes from how reliable delivery is usually implemented. A webhook sender can promise to retry until it thinks your endpoint received the event, but it cannot reliably know whether your code committed data just before a timeout or crash. That means the same payment event may be delivered again. If the handler blindly performs the work each time, a retry becomes a second charge, a second fulfilment, or a second account credit.
The concrete mechanism is to give the operation a stable identity and make the database arbitrate duplicates. For a webhook, that key might be the provider’s event identifier or a business identifier such as the payment intent. The handler records that key in a table with a unique constraint, commonly in the same transaction as the guarded state change. If insertion succeeds, it processes the event. If the key already exists, it returns success without doing the side effect again.
The common misunderstanding is that an application-level read is enough: look for the event, and process it if not found. That is unsafe under concurrency because two handlers can both read before either writes. The uniqueness check must be atomic, which usually means a unique index, an insert that can fail cleanly, or a business table whose natural key prevents duplicates. Idempotency also requires choosing the right key: too broad suppresses valid work, too narrow allows repeats.
Engineers meet this in webhook handlers, payment creation APIs, job queues, message consumers, and fulfilment pipelines. In practice, you decide what identity represents the one real-world operation, store it durably, and make duplicate delivery a normal code path rather than an exception. The handler should acknowledge a duplicate as successfully handled, because retrying it again will not improve correctness and may keep the sender in a retry loop.
Common questions
- Is idempotency the same as retrying safely?
- Not quite. Retrying is the behaviour; idempotency is the safety property that makes retrying acceptable. A retry can happen after a network failure, timeout, or crash, when neither side knows the final state. Idempotency ensures the repeated request collapses onto the original effect instead of performing the operation again.
- Why is a unique database constraint necessary?
- Because checking in application code is not atomic. Two duplicate deliveries can run at the same time, both see no existing row, and both proceed. A unique constraint lets the database make one winner and reject or ignore the other, even under concurrency, which is exactly the race the handler must survive.
- What should a webhook handler return when it sees a duplicate event?
- Usually it should return a successful response after confirming the event has already been handled. A duplicate delivery is not an error from the sender’s point of view; it is an expected result of at-least-once delivery. Returning failure may cause more retries without changing the state of your system.
- Where should the idempotency key come from?
- It depends on what operation must happen once. For provider webhooks, the provider’s event identifier can be suitable if each event maps to one effect. For business actions, a payment intent, order, invoice, or transfer identifier may be better. The key must represent the real-world operation you are preventing from happening twice.