Idempotency

An idempotent operation produces the same result no matter how many times you call it. In distributed systems where retries are inevitable, this property is the difference between a robust API and one that double charges every customer.

The problem in one sentence

Networks lose packets. Clients time out and retry. Without idempotency, retries duplicate work. The customer gets charged twice. The email goes out twice. The order is shipped twice. Idempotency is how you make a retry safe.

Formally, an operation is idempotent if calling it once and calling it ten times have the same effect. Setting a value is idempotent. Incrementing a counter is not. Deleting a record is idempotent (it is gone, deleting it again is a no-op). Creating a record is usually not, because two creates make two records.

How HTTP methods line up

MethodIdempotent?Why
GETYesReads do not change state.
PUTYesReplaces resource. Doing it twice gives the same final state.
DELETEYesOnce it is gone, deleting again is a no-op.
POSTNo (by default)Creates a new resource each time.
PATCHDepends"Set field to X" yes. "Increment field" no.

The idempotency key pattern

For operations that are not naturally idempotent (POST, payments, order placement), the common pattern is the idempotency key. The client generates a unique ID, usually a UUID, and sends it in a header like Idempotency-Key. The server stores the result against that key. If the same key shows up again within some window (typically 24 hours), the server returns the stored result instead of repeating the operation.

Idempotency Key Flow Client Server Idempotency Store (Redis) 1st request POST /charge, key=abc-123 SET key=abc-123 → charge customer once Retry (network blip) POST /charge, key=abc-123 key found → return cached ✓ no double charge

How to actually implement it

The naive version is: check if key exists, if yes return stored response, if no do the work and store the response. The race condition is obvious — two requests with the same key arrive at the same time, both check, both miss, both do the work. The fix is an atomic insert with a unique constraint on the key column. Whichever insert wins does the work, the other gets a duplicate-key error and waits, then reads the stored response.

Stripe published their approach which is the de facto standard. They store the request fingerprint, the response, and the status. If a key is reused with a different request body, they reject it as a conflict. The window is 24 hours, which covers any reasonable retry scenario but lets keys eventually expire.

Idempotency at the database layer

Sometimes you can lean on the database. INSERT ... ON CONFLICT DO NOTHING in Postgres is idempotent. PUT in DynamoDB is idempotent. Using the natural primary key (like an order ID generated by the client) as the database key gives you idempotency for free.

Rule of thumb: If a request can have side effects (charging money, sending notifications, decrementing inventory), it must be idempotent. Otherwise the first network blip is going to ruin somebody's day.