Skip to content
routebase
API Design10 chapters

Chapter 06 of 10

Idempotency and Retries

Why a timeout is ambiguous, what an idempotency key actually promises, how long a stored result has to live, and which parts of the pattern are standardised.

A client sends a create request and the connection drops before the response arrives. The client now knows nothing useful, because a timeout looks identical whether the server did the work or never received the request.

Its options are to retry and risk a duplicate, or to give up and risk losing a real order. Neither is acceptable, which is why the caller's retry behaviour is your design problem rather than theirs.

The methods that already answer this

RFC 9110 defines PUT and DELETE as idempotent, alongside the safe methods, and that guarantee is what allows a client library to retry automatically. Repeating the request must have the same effect as making it once.

Two things are worth being precise about. The guarantee is about the effect on the server rather than about the response, so a second DELETE answering 404 is still idempotent. And the guarantee is a promise you make, not a property you inherit, since nothing stops an implementation of PUT from appending a record.

What the method semantics do not cover is POST, and creating a resource is exactly where a duplicate is expensive. That gap is what idempotency keys exist to close.

What a key promises

The client generates a unique value, sends it with the request, and reuses the same value for every retry of that same intent. The server stores the key together with the outcome of the first attempt, and any later request carrying that key gets the stored response instead of new work.

The client has to be the one generating it, because only the client knows which attempts represent one intent. A server-generated value would arrive in the response the client never received.

The first attempt reaches the server and the response is lost, so the client retries with the same key and gets the stored result rather than a second order. The key is kept for a published retention window, and a retry after that window has expired is indistinguishable from a new request, which is why the window is part of the contract rather than an implementation detail.

The header name is a convention rather than a standard. The IETF HTTPAPI working group carried a draft called The Idempotency-Key HTTP Header Field, and that draft expired without being published as an RFC, which the IETF datatracker still records. So the name is common enough to be recognisable and you still have to document the semantics yourself.

The decisions inside the pattern

Adopting the pattern means answering four questions that callers will otherwise discover by experiment.

How long is a key kept? The window has to cover realistic retry behaviour and stay bounded, which lands most APIs somewhere around a day. Publish the number, because a caller needs to know when a retry stops being a replay.

What happens to a key sent with a different body? Reusing a key for a genuinely different request is a client bug. The useful answer is a 409 that says so, rather than silently replaying the old result or silently doing the new work.

What is the key scoped to? A key is normally unique per caller rather than globally, since two customers picking the same random value should not collide. Scoping it to the account and the endpoint is the common choice.

What happens if the first attempt is still running? A retry that arrives before the original finished cannot be answered from a store that has nothing in it yet. Answering 409 and asking the caller to retry shortly is honest, and returning a duplicate is not.

Making retries safe on the caller's side

Even with keys in place, the retry loop belongs to the caller, and your response is what steers it.

A 5xx or a timeout invites a retry, and a 4xx other than 429 does not, since nothing about repeating the same rejected request will change the outcome. Saying that clearly in the error body is worth more than any amount of documentation, because retry logic reads the status code.

Where you want the caller to come back, say when. Retry-After is defined in RFC 9110 and works on 429 and on 503, and it turns a guess into an instruction. That is covered further in rate limits.

In Routebase

Routebase does not implement idempotency, because it does not sit in front of your API. What it does is let you declare the key as part of the contract, in the two places a caller and a generated client will look.

The header itself is a parameter with a header location, and defining it once as a parameter component fixes the name, the type and whether it is required across every operation that accepts it. Each component lists the endpoints using it, so adding the key to a new create endpoint is a link rather than a copy.

The second place is the operation description, which is where the precondition that no schema can express actually belongs.

The description editor for the Create an order operation, with a formatting toolbar, a paragraph explaining that prices are taken from the catalog at order time, and a warning callout saying to send an Idempotency-Key header because a retried request without it creates a second order.
The rule a caller has to follow, written on the operation it applies to, where a human reader and a generated client both meet it rather than in a wiki page neither of them opens.

That description travels with the exported document, so it reaches the reference documentation and anything else generated from the contract.

A style guide rule watches the gap. A POST that declares a 201 response and accepts no idempotency key header is flagged while you edit, at informational severity and configurable per organisation and per project. The rule is anchored on the declared 201 rather than on the method, so a search or a login endpoint is not reported. The Parameters guide covers header parameters and their components, Endpoints covers the description editor, and the Style Guide covers the rule and its severity.

Frequently asked questions

What is an idempotency key?

It is a value the client generates and sends with a request so the server can recognise a retry of that exact request. The server stores the key with the result of the first attempt, and it replays that result for any later request carrying the same key. That turns an unsafe operation into one a client can repeat without creating a duplicate. The client picks the value, because only the client knows which attempts are the same intent.

Is the Idempotency-Key header a standard?

No. The IETF HTTPAPI working group had a draft named The Idempotency-Key HTTP Header Field, and that draft expired without being published as an RFC. The header name is therefore a widely used convention, so an API that accepts it has to document the semantics itself rather than pointing at a specification.

Why do idempotent HTTP methods not solve this?

They solve it for the methods they cover. RFC 9110 makes PUT and DELETE idempotent, so a client library can retry those safely, and it leaves POST and PATCH outside that guarantee. Since creating a resource is the operation where a duplicate costs the most, the case that matters most is exactly the case the method semantics do not cover.

How long should an idempotency key be stored?

Long enough to cover realistic retry behaviour and short enough that the store stays bounded, which for most APIs lands in the range of a day. The number itself matters less than publishing it, because a caller needs to know when a retry stops being a replay and starts being a second request. Whatever window you choose becomes part of the contract.

Last reviewed by The Routebase Team.

Ready to ship on it?

Routebase is live. Design your API once — docs, mocks, tests, and monitoring all follow from the same source.

14-day Pro trial — no credit card required.