The Double-Charge Problem

You tap "Pay ₹999." The spinner keeps going. The network drops, so the app retries. Did the first request reach the server? If it did, you've now paid twice.

Networks fail in the worst way: a request can succeed on the server while the response gets lost. The client can't tell "it failed" from "it worked but I didn't hear back." The fix for this is idempotency.


🎯 Quick Answer (30-Second Read)

  • Idempotent = doing something once or many times has the same effect.
  • Safe retries need idempotency: without it, retries can create duplicate orders, payments or emails.
  • HTTP: GET, PUT and DELETE are defined as idempotent. POST isn't, so "create" requests need extra help.
  • Idempotency keys: the client sends a unique key (e.g. a UUID) with the request. The server saves the result for that key and returns the same result for any retry.
  • Stripe uses an Idempotency-Key header and saves the first response for each key.

Idempotent vs Not

✅ Idempotent
"Set my address to Pune." Do it 5 times, the address is still Pune.
Pressing a lift button again.
❌ Not idempotent
"Add ₹100 to my wallet." Do it 5 times, you get ₹500.
"Create an order." Retry = second order.

HTTP Methods

Method Idempotent? Why
GET ✅ Only reads
PUT ✅ Replaces a resource with the same value each time
DELETE ✅ Deleting twice leaves it deleted (the second call may return 404, but the state is the same)
POST ❌ Usually creates something new each time
PATCH Not guaranteed Depends on the operation ("set" vs "increment")

Idempotent describes the effect on the server, not the response. A second DELETE can return a different status code and still be idempotent.


How Idempotency Keys Work

sequenceDiagram participant C as Client participant S as Server participant DB as Idempotency store C->>S: POST /payments (Idempotency-Key: 7f3a…) S->>DB: Key 7f3a seen before? DB-->>S: No S->>S: Charge ₹999 S->>DB: Save key 7f3a → 201 {payment_id: 55} S--xC: Response lost (network drop) C->>S: Retry: POST /payments (Idempotency-Key: 7f3a…) S->>DB: Key 7f3a seen before? DB-->>S: Yes → 201 {payment_id: 55} S-->>C: 201 {payment_id: 55} (no second charge)
  1. The client generates a unique key per operation, such as a UUID, and reuses it for every retry of that operation.
  2. The server checks whether the key exists.
  3. New key: process the request, then store the key with the response.
  4. Seen key: skip the work and return the stored response.

How Stripe Does It

Stripe's API is the best-known example:

  • Send Idempotency-Key: <unique value> on POST requests.
  • Stripe saves the status code and body of the first request made with a key and returns them for retries, even if the first result was a 500 error.
  • Keys can be up to 255 characters. Stripe suggests V4 UUIDs and warns against using sensitive data like email addresses as keys.
  • If a retry uses the same key with different parameters, Stripe returns an error to prevent mistakes.
  • Keys can be removed after they're at least 24 hours old. Reusing an old key after that creates a new request.
curl https://api.stripe.com/v1/customers \
  -u sk_test_...: \
  -H "Idempotency-Key: 9b2f1c4e-6a1d-4c3b-9f5e-2d8a7b6c1e0f" \
  -d description="New customer"

Building It Yourself

create table idempotency_keys (
  key          text primary key,
  user_id      bigint not null,
  request_hash text not null,          -- detect same key, different body
  status_code  int,
  response     jsonb,
  created_at   timestamptz default now()
);
stateDiagram-v2 [*] --> Lookup Lookup --> Process: key not found → insert row (in progress) Lookup --> Replay: key found + completed Lookup --> Conflict: key found + still in progress Lookup --> Reject: key found + different request body Process --> Store: save status + response Store --> [*] Replay --> [*]: return saved response Conflict --> [*]: 409, retry later Reject --> [*]: 422 error

Design details that matter:

  • Insert the key first, atomically (a unique constraint), so two simultaneous retries can't both process.
  • Do the business write and the key update in the same transaction where possible.
  • Scope keys per user so one customer's key can't collide with another's.
  • Hash the request body and reject reuse with different parameters.
  • Expire old keys (for example after 24 hours) to keep the table small.
Key takeaway: in distributed systems, retries are unavoidable, and at-least-once delivery is the norm. Idempotency is what turns "might run twice" into "safe to run twice."

Idempotency Beyond APIs

  • Message queues: consumers may receive a message twice, so they track processed message IDs. (See Message Queue Explained.)
  • Payments: UPI gives each payment a unique transaction ID so retries can't debit twice. (See How Does UPI Work?.)
  • Database writes: INSERT ... ON CONFLICT DO NOTHING makes inserts safe to repeat.
  • Infrastructure: tools like Terraform aim for idempotent runs, applying the same config twice changes nothing.

Frequently Asked Questions

What does idempotent mean in a REST API?

An idempotent API operation produces the same effect on the server whether it's called once or many times. GET, PUT and DELETE are idempotent by definition in HTTP. POST is not, so APIs often add idempotency keys to make create operations safe to retry.

How long should idempotency keys be stored?

Long enough to cover any realistic retry window. Stripe allows keys to be removed once they are at least 24 hours old, which is a common reference point. Payment systems with longer retry windows may keep keys longer.

Who generates the idempotency key, client or server?

The client generates it, because only the client knows that two requests are retries of the same operation. A random UUID per operation works well. The client must reuse the same key for every retry of that operation and use a new key for a genuinely new one.


Conclusion

Idempotency makes retries safe. Some operations are naturally idempotent, like setting a value or deleting a record. For the rest, especially creating payments and orders, idempotency keys let the server recognise a retry and return the original result instead of doing the work twice.


Sources

Last verified: 14 October 2026.


Related reads: Message Queue Explained · How Does UPI Work? · Rate Limiter Explained · Circuit Breaker Pattern Explained

I write about system design and how real products are built. Follow on LinkedIn or X, or invite me to speak.