🛗 Analogy
Idempotency is the lift-call button. Press it once or press it ten times in frustration — the lift still comes exactly once. A safe API write behaves the same: if the network swallows your reply and you press again, nothing duplicates. A non-idempotent write is a vending machine that charges you every time you re-tap, unsure if the first tap landed.
The failed POST problem
The network is unreliable. A POST that returns no reply leaves the client unsure whether the request arrived or was processed — and because POST is not idempotent, resending risks a duplicate. The Cookbook’s default answer: send all writes with PUT (idempotent) rather than POST, using the PUT-Create pattern with conditional headers.
PUT /person/q1w2e3 If-None-Match: * -> 201 CREATED + ETag
PUT /person/q1w2e3 If-Match: "<etag>" -> 200 OK + new ETag
PUT /person/q1w2e3 If-Match: "<stale>" -> 412 Precondition Failed
With PUT, the client supplies the resource identifier (like uploading a file to a known path). If-None-Match: * creates without risk of a duplicate; If-Match replaces exactly the version you read, and a mismatch returns 412 — the basis of optimistic concurrency. See caching and conditional requests for how ETags work.
graph TD
P["PUT to client-chosen URL"] --> Q{"Intent?"}
Q -->|"create"| C1{"Resource exists?"}
C1 -->|"no, If-None-Match *"| Created["201 Created"]
C1 -->|"yes"| Conflict["412 Precondition Failed"]
Q -->|"update, If-Match ETag"| C2{"ETag matches?"}
C2 -->|"yes"| OK["200 OK"]
C2 -->|"no"| ConflictTwo levels of idempotence
Safe repeats need both levels — the method is not enough on its own.
graph TD N["Network idempotence<br/>use GET / PUT / DELETE, avoid POST"] --> Safe["Safely repeatable write"] O["Operation idempotence<br/>body uses replacement values, not increments"] --> Safe
- Network idempotence — use
GET/PUT/DELETE; preferPUTto create. APUTthat fails with503is safe to repeat. - Operation idempotence — design the write itself to be idempotent. A
priceUpdatebody ofupdatePercent=.05is dangerous: applied twice on a partial failure it compounds. Redesign it astext/csvrows ofproductId, currentPrice, newPriceso the service appliesnewPriceonly when the stored price still equalscurrentPrice, skipping records already updated.
Don't trust the method alone
An idempotent HTTP method with a non-idempotent body is still unsafe. Make the payload conditionally applied (replacement values, not increments or percentages) so a crashed batch can be re-run from the start without double-applying anything.
Safety: GET must not mutate
A safe method has no side effects on server state — fundamentally, GET. This guarantee (part of Richardson Level 2) is what lets caches, proxies, and crawlers replay GETs freely. Reserve state changes for PUT/POST/DELETE, and only ever auto-retry idempotent methods (GET, HEAD, PUT, DELETE) — never POST or PATCH.
Reversible actions
Sometimes you must undo. With no HTTP UNDELETE, design reversal explicitly:
- Issue a second request — re-
PUTthe prior value withIf-Match(requires knowing the prior value and that no one else changed it). - Issue a special request — e.g. recover a
DELETEvia anundoDeleteoperation (PUTto a rollback URL), which requires retaining deleted resources.
Idempotency keys for POST
POST can be made idempotent with an idempotency key header so the server recognises and de-duplicates a retried request. The Cookbook’s author still defaults to PUT — but the key approach is the standard escape hatch when a true PUT-create does not fit.
See also
- Caching and conditional requests — ETags and If-Match in depth.
- REST and the Richardson Maturity Model — verb safety semantics.
- Hypermedia and HATEOAS — designing idempotent actions into forms.
When to use it — and when not
✅ Reach for it when
- A write may be retried after a lost response, timeout, or network blip.
- You are processing batches where a mid-run crash must be safely re-run.
- You need optimistic concurrency to avoid clobbering another client's update.
⛔ Think twice when
- A genuinely non-repeatable action where you instead need an idempotency key on POST.
- Read-only endpoints — GET is already safe; no extra machinery needed.
Related topics
REST's constraints and the four levels of the Richardson Maturity Model — and why Level 2 is the practical sweet spot for most HTTP APIs.
api-restCaching and Conditional RequestsUse HTTP caching directives and ETags to make remote data feel fast and writes safe — providers set Cache-Control tuned to volatility, consumers qualify with freshness rules.
api-restHypermedia and HATEOASEmbedding links and forms in responses lets clients discover what to do next at runtime — the engine of application state, and the key to evolvable services.
Check your understanding
Score: 0 / 41. What is the 'failed POST' problem the Cookbook describes?
Because POST is not idempotent, a lost response means resending might create a duplicate — so the author defaults writes to PUT.
2. How do you create a resource idempotently with PUT?
PUT-Create: the client supplies the identifier and uses If-None-Match:* so a repeat does not create a duplicate; mismatch on update returns 412.
3. Why is the HTTP method alone not enough for safe repeats?
A PUT carrying 'apply 5%' is dangerous on partial failure; redesign the payload to carry replacement values applied only when the stored value still matches.
4. Which status code signals an optimistic-concurrency conflict on a conditional PUT?
If-Match against a stale ETag returns 412 Precondition Failed, telling the client to GET, merge, and retry.
Comments
Sign in with GitHub to join the discussion.