Direct answer. GET, HEAD, and OPTIONS are safe (they must not change server state) and idempotent (calling them N times has the same effect as calling them once). PUT and DELETE are idempotent but not safe. POST is neither safe nor idempotent by default. PATCH is technically unspecified but should usually be treated as non-idempotent unless you deliberately design it to be. Cacheability tracks safety closely but is not identical to it: GET and HEAD are cacheable by default, OPTIONS is safe but has no real caching convention, and none of PUT, DELETE, POST, or PATCH are cacheable by default.
The full classification.
| Method | Safe | Idempotent | Cacheable | Typical use |
|---|
| GET | yes | yes | yes, by default | read a resource, no side effects |
| HEAD | yes | yes | yes, by default | GET without a body, for existence/metadata checks |
| OPTIONS | yes | yes | no | discover allowed methods, CORS preflight |
| PUT | no | yes | no | replace a resource entirely at a known URI |
| DELETE | no | yes | no | remove a resource; deleting twice leaves the same end state (gone) |
| POST | no | no | no by default | create a new resource, or trigger a non-idempotent action |
| PATCH | no | usually not | no | partially update a resource |
Why cacheability does not just follow safety. GET and HEAD are cacheable by default because a cache can reuse their response without risking a stale side effect, the same property that makes them safe in the first place. OPTIONS is also safe, but nothing about discovering allowed methods or a CORS preflight benefits from caching the way a resource representation does, so there is no real caching convention for it in practice. POST responses CAN technically be cached per the HTTP spec if the response carries explicit freshness information (Cache-Control or Expires), but this is rarely implemented, so treat POST as effectively not cacheable. PUT, DELETE, and PATCH have no meaningful default caching semantics either: caching the result of a mutation makes little sense when the whole point of the call was to change state.
Why this matters beyond vocabulary. Idempotency is the property that makes retries safe. If a client's network call to a PUT times out and it retries, the end state is identical whether the first request actually landed or not, because PUT is defined as "the resource now looks like this", not "apply this delta". A POST retried the same way can create two resources, because POST means "do this action again", and doing a creation action twice creates two things. Safety is what a cache, a browser prefetcher, or a crawler relies on: none of them should ever issue a POST speculatively, because a safe method is one where the caller assumes no side effect happened.
A real bug from getting this wrong. A checkout flow implemented "add item to cart" as a GET request (because it was convenient to trigger from a link). A corporate web-security scanner crawled every link on the page, including that one, adding dozens of items to real users' carts, because the scanner (correctly, per the HTTP contract) assumed GET was safe to call without consequence. The fix was not to block the scanner, it was to make cart mutation a POST, which is exactly what the safety property exists to protect against.
Trade-offs and pitfalls. The subtlest mistake is assuming PATCH is idempotent by default. A PATCH body of {"counter": "increment"} is not idempotent (retrying it increments twice); a PATCH body of {"counter": 5} (set to an absolute value) is. The method name alone does not tell a client which one they are getting, so this needs to be documented per endpoint, not assumed from the HTTP verb.