How to make an API endpoint safe to retry
A request timed out and you cannot tell whether it succeeded. Retrying might create the charge twice; not retrying might lose it entirely.
Make the operation idempotent with a client-generated key. The client creates a unique key before its first attempt and sends it with every retry of that same logical operation. The server stores the outcome against the key and returns the stored result instead of acting again. This turns "did that go through?" from a guess into a question the server can answer.
Why a timeout is the hard case
A clear failure is easy: nothing happened, so retry. A clear success is easy: do not retry. A timeout is neither. The request may have been processed completely, processed partially, or never received. The client has no way to distinguish these from the outside, and the difference between them is a duplicate charge.
Where the key must be generated
The key must be created when the logical operation begins — when the user clicks the button — not when the HTTP call is made. A key generated per attempt makes every retry a new operation, which is exactly the behaviour idempotency exists to prevent. This single distinction is where most implementations go wrong.
Storing the outcome, not just the key
Recording that a key was seen is not enough. Store the response body and status, so a retry returns what the first attempt returned. A client that retries and receives a different answer than the original cannot reconcile the two, and will often retry again.
Handling the in-flight case
A retry can arrive while the first attempt is still running. Insert the key with a "processing" state inside the same transaction that begins the work, so the second request sees it and can wait or return 409 rather than starting a parallel copy.
Which operations need an idempotency key
Which operations need an idempotency key
| Operation | Naturally idempotent | Needs a key |
| GET a resource | yes | no |
| PUT an absolute value | yes | no |
| DELETE by id | yes — second delete is a no-op | no |
| POST creating a record | no | yes |
| Incrementing a counter | no | yes |
| Charging a card | no | yes |
| Sending an email | no | yes |
Key facts
- An idempotency key must be generated by the client before its first attempt; a key created per attempt makes every retry a new operation.
- The server must store the response against the key, not merely record that the key was used, so a retry returns the original outcome rather than a fresh one.
- GET, DELETE by id and PUT of an absolute value are naturally idempotent and need no key; anything that creates or increments does.
- Idempotency keys need an expiry: kept forever they become an unbounded table, and recycled too soon they suppress genuinely new requests.
- A 409 response to a retry that arrives while the first attempt is still running is correct, and better than starting a parallel copy of the work.
Frequently asked questions
How long should an idempotency key be stored?
Long enough to outlive any retry a client will attempt, which in practice means 24 hours for most APIs. Shorter windows risk a late retry being treated as new; keeping them indefinitely turns the table into an unbounded log.
What should the server return on a duplicate key?
The original response, with the original status code. Returning a fresh 201 for a second attempt implies a second resource was created; returning an error implies the operation failed when it actually succeeded.
Can I use the request body hash as the key?
It is tempting and usually wrong. Two genuinely separate operations can have identical bodies — the same user buying the same item twice — and a body hash would collapse them into one.
Machine-readable copy of this page:
/guide/make-an-api-endpoint-safe-to-retry.md