Internet Protocols · HTTP

Method Semantics: Safe, Idempotent, and Cacheable Requests

What the three method properties actually license a client, cache, or proxy to do, and where the definitions stop matching what real methods do.

A method is not a verb attached to a URL. It is a promise about what an intermediary may do with the message without asking the origin server. The properties are stated in RFC 9110 as three separate adjectives, and each one answers a different question: may a cache store the response, may a client retry after a network failure, and may anything at all be done speculatively.

Safe does not mean harmless

A safe method is one whose defined semantics are read-only: the client did not request a state change and does not expect one. GET, HEAD, OPTIONS, and TRACE are safe. The definition is about what the client asked for, not about what the server does. A GET handler that increments a view counter has still used a safe method, and the specification’s guarantee is correspondingly weaker than it sounds: the client is entitled to assume no change was requested, and an intermediary is entitled to prefetch or repeat the request.

That entitlement is the practical content of safety. Crawlers, browser prefetchers, and link scanners issue GET requests to URLs they have never seen. A site that performs a destructive action on GET will eventually have that action performed by something that was only following links. The specification’s answer is not that servers must be pure but that servers must not attach significance to GET beyond retrieval; if an action needs to happen, it belongs to a method that is not safe.

HEAD is safe for the same reason and is unusual in one respect: the field section should be identical to what GET would return, minus the body. That includes Content-Length, which must equal the length the body would have had. A server that computes HEAD by running a separate, cheaper code path tends to drift, and the drift is visible to clients that use HEAD to decide whether to fetch.

Idempotent is about repetition, not about results

An idempotent method is one where repeating an identical request has the same effect on server state as sending it once. PUT, DELETE, and the safe methods are idempotent; POST is not. The definition is deliberately about effect rather than response: a second identical DELETE may return 404 instead of 204, and it is still idempotent because the state after the second request equals the state after the first.

This property is what makes automatic retry defensible. When a client sends a request and the connection dies before a response arrives, the client cannot tell whether the server processed it. For an idempotent method, resending is harmless at the level of state, so a retry is a reasonable default. For POST, the retry may create a second order, a second payment, or a second comment, and no amount of transport cleverness fixes that. The remedy is at the application layer: an idempotency key carried in a header or body, checked by the origin server, which converts a non-idempotent method into an effectively idempotent operation. That mechanism is a convention, not part of HTTP.

The distinction also constrains intermediaries. A proxy that retries a request after an upstream timeout must not do so for POST without knowing the application’s semantics; the defensible case for an automatic retry is a failure at connection establishment, where the request provably never reached the origin.

Cacheable is a property of the response too

Cacheability is the third property, and it is the one most often misread as a property of the method alone. RFC 9110 defines a cacheable method as one whose responses may be stored, and names GET and HEAD as cacheable, POST as cacheable only when the response carries explicit freshness information and a Content-Location field whose value equals the request target, and PUT and DELETE as not cacheable. In practice, whether a specific response is stored depends on the response’s own Cache-Control, on whether the status code is defined as heuristically cacheable, and on whether the request method is one the cache is willing to handle.

The interaction that matters is invalidation. A cache that stores GET /widgets/7 must invalidate that entry when an unsafe method produces a non-error response for the same URI. RFC 9111 requires invalidation of the target URI and permits, but does not require, invalidation of the URIs named in Location and Content-Location; invalidating those is forbidden outright when their origin differs from the target’s. Nothing else has to be invalidated, so a resource whose representation changes as a side effect of a write to a different URI can serve stale data with no protocol violation. Sites that expose collections and members as separate URIs are exposed to that gap.

Where the model leaks

Four places where the clean three-property model meets reality.

PATCH is neither safe nor idempotent in general. RFC 5789 defines it as a method carrying a patch document, and notes that whether a given patch is idempotent depends on the patch format and on the document. A patch that sets a field to a fixed value produces the same state when applied twice; one that appends to a list does not. The method’s own definition therefore cannot promise idempotence, and a client retrying a PATCH is making an assumption about the media type.

GET with a body is undefined rather than forbidden. The grammar allows a body on any request, but RFC 9110 says a GET request’s content has no defined semantics, and servers are free to ignore it. Intermediaries may therefore drop it. A design that depends on a GET body is depending on behaviour no specification guarantees.

OPTIONS and TRACE are safe but rarely useful in the way their definitions suggest. OPTIONS with * as the target asks about the server as a whole, and a server may answer with a fixed set of allowed methods rather than anything derived from the resource. TRACE reflects the request back, which historically made it usable for cross-site tracing: a script that could trigger the reflected message could read the fields of a request it could not otherwise inspect.

Conditional writes are the practical bridge between idempotence and concurrency. A PUT with If-Match is idempotent and also refuses to overwrite a representation that changed since the client read it. That combination is what conditional requests and validators provide, and it is the reason the lost-update problem has a protocol-level answer rather than only an application-level one.

Choosing a method as a design act

The method choice determines what every intermediary in the path is allowed to assume, and that set of assumptions is not recoverable later. A retrieval endpoint that uses POST to avoid caching cannot be cached, cannot be safely prefetched, cannot be retried automatically, and cannot be bookmarked; the costs are paid by every client and every hop.

The inverse mistake is overloading GET with state changes to gain cacheability. The result is an endpoint that a shared cache may serve from storage after the state has changed, because a cache has no way to know that the URL’s meaning included a mutation. Between the two, the safer error is the uncacheable POST: it is slow but honest.

A useful test when designing an endpoint is to state the three properties explicitly and check that the handler agrees. If the handler is not safe but the method is, the handler is wrong. If the method is idempotent but the handler produces a distinct resource identifier on each call, the handler is wrong. If the response is cacheable but the handler varies on a request field that is not listed in Vary, the response is wrong in a way that only shows up behind a shared cache.

References

  1. RFC 9110: HTTP Semantics — IETF
  2. RFC 5789: PATCH Method for HTTP — IETF
  3. Architectural Styles and the Design of Network-based Software Architectures — Roy Fielding, doctoral dissertation, UC Irvine, 2000