The duplicate-request problem
A client sends POST /payments, the payment succeeds, but the response is lost to a network timeout. The client, seeing no response, retries — and now you have charged the customer twice. This is one of the most common and costly API bugs. The fix is idempotency.
What idempotency means
An operation is idempotent if performing it multiple times has the same effect as performing it once. GET, PUT, and DELETE are naturally idempotent. POST is not — each call creates something new. To make POST safe to retry, you add an idempotency key.
How idempotency keys work
The client generates a unique key (a UUID) per logical operation and sends it in a header:
POST /payments
Idempotency-Key: 5b1f...c9
Content-Type: application/json
{ "amount": 4995, "currency": "INR" }The server's logic:
1. Look up the idempotency key.
2. If seen before -> return the SAME stored response. Do not re-execute.
3. If new -> execute, store the response against the key, then return it.Now a retry with the same key returns the original result without charging again.
{ "id": "pay_91", "status": "succeeded", "amount": 4995 }Implementation details that matter
- Store the response, not just the key. A retry must get the exact original response (same body, same status), so persist both.
- Scope keys correctly. Tie a key to a specific endpoint and account so unrelated requests cannot collide.
- Handle in-flight duplicates. If a second request arrives while the first is still processing, lock on the key and make the second wait or return a "request in progress" status.
- Expire keys. Keep them for 24 hours or so, then purge — they are for retry windows, not forever.
Idempotency vs uniqueness constraints
A database unique constraint (one order per cart) prevents duplicates at the data layer, but the client still gets an ugly error on retry. Idempotency keys are better because the retry transparently returns the original success. Use both: the key for a smooth client experience, the constraint as a safety net.
Designing idempotent endpoints
- Make
PUTreplace the whole resource (idempotent by design) instead ofPOSTwhere you can. - For
POST, require or support anIdempotency-Keyheader on anything with side effects (payments, emails, provisioning). - Document clearly which endpoints honor idempotency keys and how long they are retained.