HTTP Methods Are Promises
Safe means "calling this changes nothing"; idempotent means "calling this twice equals calling it once". Retrying clients, proxies, caches and crawlers all act on those promises without asking — which is why breaking them breaks things you have never heard of.
Frame the contract
API design starts with a consumer, a design question and a guarantee — never with a URL.
Two properties carry all the weight
Strip HTTP methods of folklore and two properties remain. Safe: the request causes no state change the client can be held responsible for — GET and HEAD promise this. Idempotent: N identical requests have the effect of one — GET, PUT and DELETE promise this; POST pointedly does not. Everything practical follows: what may be retried without asking, what may be cached, what a prefetcher may touch, what a proxy may replay after a dropped connection.
The properties are about *effects*, not responses. DELETE is idempotent even though the second call may return 404 where the first returned 204 — the resource is equally gone either way (see DELETE: What Does Gone Mean?). A GET whose handler increments a view counter is arguably still safe — the client did not ask for or take responsibility for that side effect. The line is: would repeating or prefetching this request change what the client is accountable for?
These are promises *you* make by choosing the method — HTTP does not enforce them. The router will happily bind state mutation to GET. What enforces the promises is the ecosystem: everything between your handler and the user was built assuming they hold, and it acts accordingly, silently, at scale.
| Method | Safe | Idempotent | Acted on by |
|---|---|---|---|
| GET | yes | yes | Caches store it, prefetchers fetch it, retries fire freely, crawlers follow it |
| HEAD | yes | yes | Link checkers, caches revalidating |
| PUT | no | yes | Retry middleware resends on timeout without a key |
| DELETE | no | yes | Retry middleware resends; second 404 is expected (see DELETE: What Does Gone Mean?) |
| POST | no | no | Nothing retries it blindly — this is the method's value, not a defect (see POST: More Than Create) |
| PATCH | no | not guaranteed | Retried only when you add idempotency (see PUT vs PATCH) |
The invisible audience
The reason method semantics outrank naming taste: the audience for them is mostly software you will never meet. A mobile SDK's retry interceptor resends idempotent methods on timeout by default. A corporate proxy that lost the upstream connection mid-request replays it — if the method claims idempotency. A CDN caches GET responses because GET said "safe" (see Caching as a Contract Clause). A browser preloads a link the user might click. None of these consulted your documentation; they consulted the method.
This audience is also why retries are the load-bearing case. Networks lose responses — a timeout does not tell the client whether the request executed (see Retries and Timeouts as Contract Guidance). The entire recovery strategy of every generic client is method-shaped: idempotent → resend; POST → do not resend without an application-level guarantee like an Idempotency Keys: The Mechanism key. Declare the wrong method and you have set every generic client's recovery strategy wrong, everywhere, at once.
- Retry middleware resends GET/PUT/DELETE on timeout by default — your semantics decide whether that is recovery or corruption.
- Proxies and load balancers may replay idempotent requests after connection failure, per spec.
- Caches (browser, CDN, corporate) store and serve GET responses; a mutating GET can be *served from cache* — mutation silently skipped.
- Prefetchers and crawlers issue GETs to anything visible, on no one's command.
- Monitoring probes GET health and sample endpoints continuously; unsafe GETs turn observability into load-bearing writes.
Lying to the infrastructure
The everything-POST API (each operation tunneled through POST /doThing) does not break — it *forfeits*. Nothing can be cached, nothing can be safely retried, every intermediary must assume the worst about every call. Failures that idempotent methods absorb automatically become manual incident response; the client teams each build their own retry-with-dedup layer, differently and wrong. The opposite lie — mutating GETs — fails louder and stranger: prefetchers fire the mutation, caches absorb it, crawlers trigger it row by row (the canonical incident lives in GET: The Promise of Safety).
The design discipline is one question per operation, asked at review: *what happens when this exact request arrives twice?* If the honest answer is "the same end state" — declare an idempotent method and get the ecosystem's recovery for free. If it is "a second effect" — that is POST, and the operation needs an application-level idempotency answer before it carries anything valuable (see Idempotency). The method line in your spec is not a style choice; it is the machine-readable summary of that answer.
1POST /api/doAction { "action": "getUser", "id": 42 }2POST /api/doAction { "action": "deleteUser", "id": 42 }3POST /api/doAction { "action": "renewLease", "id": 42 }4 5# reads: uncacheable, unprefetchable6# deletes: unretryable without custom dedup7# every intermediary assumes maximum danger8# a timeout on ANY call → manual recovery1GET /users/42 # cached, prefetched, retried freely2DELETE /users/42 # retried on timeout; 2nd call 404 = done3PUT /leases/42 # full-state renewal: resend = same state4POST /users # not idempotent — so it carries5Idempotency-Key: 8a1f… # an application-level answer insteadThe right column is not more RESTful — it is more *legible to machines*. Every retry policy, cache and proxy between handler and user now makes correct decisions without coordination. The left column forces every one of them to either assume the worst or guess.
Key points
- Safe and idempotent are the only two properties that matter; every operational behavior (retry, cache, prefetch) derives from them.
- Idempotency is about end-state effects, not identical responses — DELETE returning 404 the second time is still idempotent.
- The audience for method semantics is mostly infrastructure you will never meet, acting without reading your docs.
- Declaring the wrong method sets every generic client's failure recovery wrong at once.
- Ask per operation: "what happens when this request arrives twice?" — the method is the machine-readable form of the answer.
- Everything-POST forfeits the ecosystem; mutating GETs weaponize it.
Method Semantics Under Retry
Change the contract and observe which guarantee moves.
Follow the failure
How the contract fails or gets misused, hop by hop — and what it costs when it completes.
- 1Team → API: tunnels all operations through POST because "the framework routes on the body anyway".
- 2Mobile SDK → API: disables retries entirely — nothing is marked idempotent — so every network blip becomes a user-visible error.
- 3Client team → SDK: builds custom retry-with-dedup for the worst endpoints; three clients, three dedup dialects.
- 4Ops → API: nothing is cacheable, so read traffic hits origin at full volume; a traffic spike becomes an outage a CDN would have absorbed.
- 5Team → post-incident: retrofits GET/PUT/DELETE semantics; every client's hand-rolled recovery layer must now be unwound.
- Transient network failures surface as user-visible errors or duplicated effects, depending on which wrong guess each client made.
- Caching and prefetching are forfeited (everything-POST) or corrupted (unsafe GET), losing the web's free performance layer.
- Every intermediary between client and server must treat every request as dangerous, disabling the recovery behavior HTTP was designed to give.
Design, observe, evolve
A contract decision is incomplete until you know how you would notice it failing and how it changes later.
- • Map each operation to the method whose safety/idempotency promises are actually true for it; audit the map in API review.
- • Give non-idempotent operations (POST) an application-level idempotency mechanism before they carry money or irreversible effects (see [[idempotency-keys]]).
- • State retry guidance per operation in the docs — method semantics plus [[retryability]] signals make client recovery deterministic.
- • Never overload one endpoint with mixed semantics (a GET that sometimes writes, a POST that sometimes just reads) — split them.
- • Duplicate side effects correlated with client timeouts reveal idempotency promises that the implementation does not keep.
- • Writes appearing in access logs from crawler/prefetcher user agents mean a safe method is mutating.
- • A cache-hit ratio near zero across the API is the everything-POST tax, measurable in origin load.
- • Method semantics are the most frozen clause in HTTP — changing an operation's method is a breaking change for every client and intermediary, so migrate by adding the new operation alongside (see [[api-migration]]).
- • Semantics can be *strengthened* compatibly: making a POST handler internally idempotent breaks no one and quietly de-risks every existing retry.
- • Weakening is the forbidden direction: a PUT that stops being idempotent invalidates recovery behavior consumers already deployed.
- • Honoring PUT/DELETE idempotency constrains implementations — handlers must tolerate replays and out-of-order arrivals, which costs design effort.
- • Splitting mixed-semantics endpoints multiplies routes and can turn one call into two for some clients.
- • Strict method discipline occasionally fights tooling (HTML forms, ancient proxies, gRPC-web bridges) that only speak GET/POST — the workarounds need documenting.