Skip to main content
APIs

Idempotency in APIs: make retries safe before the network makes them necessary

An idempotent operation can be repeated without changing the result beyond the first successful call. Use idempotency keys for writes that clients may retry after timeouts.

Thien Nguyen
By Thien Nguyen
Updated July 21, 2026 · 3 min read

When a client's POST /charges times out, it has no way to know whether the charge went through — the response never arrived, but the server may have finished the work. So it retries. The only thing standing between that retry and a second charge is whether the operation is idempotent: safe to repeat because a duplicate request produces the original result instead of a new side effect. Build that in before the network forces the question, because timeouts are not an edge case — they're a Tuesday.

The mechanism is a client-generated idempotency key. The client sends a unique key with each logical operation and reuses the same key on every retry of that operation. The server stores the key alongside a fingerprint of the request and the response it produced, so a duplicate returns the stored response instead of executing again.

POST /orders
Idempotency-Key: 0f3e8c2a-...
Retry seesServer response
Same key, same payloadOriginal stored result
Same key, different payload409 Conflict — key reused for a different request
New keyProcess once, store result

Idempotent is not the same as safe

These two HTTP properties get conflated, and the difference decides which operations even need a key:

PropertyMeansMethods
SafeNo observable state change at allGET, HEAD, OPTIONS
IdempotentRepeating yields the same resultGET, PUT, DELETE, and POST with a key

Every safe method is idempotent, but not every idempotent method is safe: DELETE /orders/42 changes state, yet calling it five times leaves the same single deletion. POST is neither by default — which is exactly why it's the verb that needs an explicit idempotency key bolted on.

PUT is nominally idempotent, but the guarantee lives in your handler, not the verb. A PUT that appends a row or bumps a counter on every call is not idempotent no matter what the RFC says. The method sets the expectation; your code has to honor it.

The race the naive version misses

Storing key → response isn't enough on its own. Two retries can arrive milliseconds apart — the first hasn't finished writing its result when the second checks the store, finds nothing, and starts a second execution. Now you've charged the card twice despite having idempotency "implemented."

The fix is to make the key claim atomic. Insert the key with an in-progress status under a unique constraint before doing the work; a concurrent retry that hits the duplicate-key error either waits or gets a 409. Mark it completed with the stored response once the work lands. Give the record a retention window that matches real retry behavior — Stripe, for reference, expires idempotency keys after 24 hours — and protect that key store with the same durability as the operation it guards. An idempotency layer that loses its keys on a restart is a double-charge waiting for a deploy.

Cover photo by Stanislav Kondratiev on Pexels.

References

Primary documentation and specifications checked when this article was last updated.

APIsReliabilityPayments

Related articles

All articles