Idempotency and Idempotency Keys

Idempotency in APIs and consumers: idempotency keys, dedup tables, safe retries for payments, and designing idempotent operations.

Intermediate 3 min read · Updated

An operation is idempotent if doing it several times has the same effect as doing it once. In distributed systems, retries are unavoidable (timeouts, dropped connections, redelivered messages), and idempotency is what makes those retries safe: a user who taps "Pay" twice, or a client that retries after a timeout, must not be charged twice.

Naturally idempotent vs not

OperationIdempotent?Why
GET /orders/42YesReading changes nothing
PUT /users/42 {email: "a@b.com"}YesSetting a value to X twice leaves it X
DELETE /carts/7Yes (in effect)Deleting something already deleted leaves it deleted
POST /payments {amount: 500}NoEach call creates a new payment
UPDATE balance = balance - 500NoEach call subtracts again
UPDATE balance = 1500 WHERE version = 7YesThe conditional write applies at most once

Idempotency keys

For operations that aren't naturally idempotent, the client generates a unique idempotency key (a UUID) per logical action and sends it with every retry. The server stores the key with the result and, on a repeat, returns the stored result instead of acting again.

ClientPayments APIKey storeCard networkPOST /charges, Idempotency-Key: k-7f3insert k-7f3 (status: in progress)charge ₹500k-7f3 → charge ch_91 (done)201 ch_91 (response lost)retry, same key k-7f3lookup k-7f3 → ch_91201 ch_91 (replayed, no new charge)
The first attempt succeeds but the response is lost. The retry carries the same key, so the server returns the original result instead of charging again.

Implementation details that matter

  • Atomically claim the key before doing the work (a unique constraint or SET NX), so two concurrent retries can't both proceed. A second request that finds the key "in progress" should wait or get a 409.
  • Store the response (status and body) and replay it exactly for repeats.
  • Scope keys per client or account, and expire them after a sensible window (Stripe keeps them 24 hours).
  • Validate the payload: if a key is reused with different parameters, reject it rather than replaying an unrelated result.
  • Record the key in the same transaction as the business change where possible, so "did it happen?" and "is the key stored?" can't disagree.

Idempotent consumers

Message consumers face the same problem because queues deliver at least once. Common techniques:

  • Deduplication table: store processed message IDs with a unique constraint, in the same transaction as the side effect. See delivery guarantees.
  • Natural keys: INSERT … ON CONFLICT DO NOTHING on a business key (one shipment per order ID).
  • Conditional updates and versions: apply an event only if its version is newer than the stored one, which also handles out-of-order delivery.
  • Set state, don't increment: "order status = shipped" is idempotent; "increment shipped count" is not.

Designing idempotent APIs

  • Accept an Idempotency-Key header on every POST that creates something or moves money.
  • Let clients create resources with client-generated IDs (PUT /orders/{uuid}), which turns creation into an idempotent upsert.
  • Make retries safe end to end: if your service calls another (a payment provider), pass an idempotency key derived from yours.

Where it’s used

Stripe

Every POST accepts an Idempotency-Key; repeated requests with the same key return the original response for 24 hours.

Payment and ride-booking apps

Double taps and flaky mobile networks are handled with client-generated keys so a ride or payment is created once.

Kafka consumers writing to databases

Upserts keyed by event or entity ID make reprocessing after a rebalance harmless.

AWS APIs

Many create calls take a ClientToken so retried requests don't launch duplicate resources.

In an interview

  • Any time you add retries or a queue, say how duplicates are prevented: idempotency keys, dedup tables or natural keys.
  • For payments and orders, propose an Idempotency-Key header and explain storing the response.
  • Claim the key atomically to handle concurrent retries.
  • Prefer state-setting operations over increments in event handlers.