What this usually means
Idempotency keys prevent duplicates by storing the key along with the response from the first request. When a retry arrives with the same key, the server returns the stored response instead of processing again. This breaks when: (1) the key is stored after the operation instead of before — a race condition where two requests both check and find no existing key, (2) the stored response has a short TTL and expires before the retry arrives, (3) the key storage and the operation are not atomic — one can succeed while the other fails.
The first ten minutes \u2014 establish facts before touching code.
- 1Check if the idempotency key is being stored before or after the operation. It must be stored before.
- 2Check the database for duplicate idempotency keys. If the same key appears twice, the uniqueness check is not working.
- 3Check the idempotency key TTL. If it expires before a retry can arrive, duplicates are possible.
- 4Check if the idempotency check and the operation are in the same database transaction. If not, one can happen without the other.
- 5Check if the payment provider (Stripe, PayPal) has its own idempotency. Stripe accepts idempotency keys — forward the key to Stripe.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchIdempotency key storage — database table, cache, or in-memory store
- searchKey storage code — is it an atomic 'check and set' or a race-condition-prone 'check then set'?
- searchDatabase schema — is there a UNIQUE constraint on the idempotency key column?
- searchPayment provider integration — are you forwarding the idempotency key to Stripe/PayPal?
- searchError handling — if the operation fails, is the stored key cleaned up so a retry with the same key can re-attempt?
- searchKey TTL configuration — how long are stored responses kept?
Practical causes, not theory. These are the things you will actually find.
- warningIdempotency check is done on read, then write — two concurrent requests both see no key and both proceed
- warningMissing UNIQUE constraint on the idempotency key column — database allows duplicate keys
- warningIdempotency key is stored after the operation instead of before — crash after operation but before storage
- warningStored response TTL is too short — expires before the client's retry window closes
- warningKey is stored but the response is not — retry gets 'key found' but no cached response, so it re-processes
- warningThe payment provider is not receiving the idempotency key — the provider processes it as a new charge
- warningKey generation is not deterministic — the same logical operation gets different keys on retry
Concrete fix directions. Pick the one that matches your root cause.
- buildStore the idempotency key with a UNIQUE constraint in the database before performing the operation
- buildUse an atomic 'insert if not exists' operation — if the key already exists, return the stored response
- buildForward the idempotency key to the payment provider (Stripe, PayPal) so they handle deduplication at their end
- buildSet the idempotency key TTL to exceed the maximum expected retry window (at least 24 hours for payments)
- buildIf the operation fails, delete the stored key so a retry with the same key can re-attempt the operation
- buildAdd a database-level UNIQUE constraint on the payment reference or order ID as a safety net
A fix you cannot prove is a guess. Close the loop.
- verifiedSend two concurrent requests with the same idempotency key. Confirm only one operation is processed.
- verifiedSend a request, let it succeed, then send the same idempotency key again. Confirm the cached response is returned.
- verifiedCheck the database — the idempotency key should appear exactly once.
- verifiedCheck the payment provider dashboard — only one charge should exist.
- verifiedTest with a simulated crash after storing the key but before completing the operation. The key should be cleaned up.
Things that make this bug worse or harder to find.
- warningStoring the idempotency key after the operation completes
- warningNot using a database UNIQUE constraint on the idempotency key
- warningSetting a short TTL on stored idempotency responses
- warningNot forwarding the idempotency key to the payment provider
- warningTreating the idempotency key as optional — it is critical for payment safety