All guides

LEARN \u00b7 DEBUGGING GUIDE

Idempotency key not preventing duplicate payment: how to debug it

A customer clicks 'Pay' once. A network hiccup causes the client to retry with the same idempotency key. The server processes the charge twice. The idempotency key should have prevented this.

AdvancedAuth/session/payment bugs

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.

( 01 )Fast diagnosis

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.
( 02 )Where to look

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?
( 03 )Common root causes

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
( 04 )Fix patterns

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
( 05 )How to verify

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.
( 06 )Mistakes to avoid

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