HTTP defines GET as a "safe" method. What exactly does safety promise, what breaks in production when a GET endpoint changes state, and are server-side effects like logging a violation?
answer
- Safe = client requests no change; user not accountable
- Logging, counters, cache-warming = allowed
- Crawlers, prefetch, link previews, mail scanners fire GETs
- GET mutations also get auto-retried and cached
- HEAD mirrors GET; OPTIONS fires on CORS preflight
basics
~20 sSafe means the client does not request any state change, so the user cannot be held accountable for side effects. Server-side bookkeeping — logs, hit counters, cache warming — is allowed because the user didn't ask for it. Mutating GETs get triggered by crawlers, prefetchers and link-preview bots.
solid answer
~50 sRFC 9110 §9.2.1 says a safe method is "essentially read-only": the client does not *request* a state change. Safety is about intent and accountability, not about the server literally staying frozen. So **logging, analytics counters, cache warming and rate-limit bookkeeping are not violations** — the user did not ask for them and cannot be held responsible for them. What violates safety is an endpoint where the *point* of the request is to change something: `GET /accounts/42/delete`, `GET /unsubscribe?id=…`, `GET /cart/add?sku=…`. What breaks in practice: search crawlers follow every GET they discover; browser and link prefetchers fetch before the user clicks; chat and mail clients fetch link previews; corporate security scanners crawl mail. Any of them can silently execute your mutation, at scale, with no user action. Anti-virus scanners walking an inbox have wiped accounts this way. The fix is to move the mutation behind POST, PUT, PATCH or DELETE — the methods whose contract admits state change.
go deeper
Define safe as 'the client isn't asking to change anything' and give the classic example of a delete link being crawled.
Separate requested change from incidental server bookkeeping, and list the real traffic sources — crawlers, prefetch, link previews, mail scanners — that fire GETs unprompted.
Add the compounding failures: auto-retry of idempotent methods and shared-cache interception, plus HEAD/OPTIONS mirroring, and describe how you would audit an existing API for mutating GETs.
Frame safety as the contract exposed to unknown intermediaries and treat violations as a security/availability class — how you detect them at the gateway, prevent them in review, and reason about which incidental effects are acceptable at crawler scale.
## What safety actually promises RFC 9110 §9.2.1 defines safe methods as those where the request is "essentially read-only" — the client **does not request** any state change on the origin server. The spec then adds the crucial clarification: this does not mean the server can't do anything, only that "the client cannot be held accountable" for side effects the request happens to cause. GET, HEAD, OPTIONS and TRACE are safe. Safety implies idempotency (a request that intends no change converges trivially), but the reverse is false — PUT and DELETE are idempotent and emphatically not safe. ## Why the property exists at all Safety is not an aesthetic preference. It is the contract that lets **software with no knowledge of your application** issue requests freely. A huge amount of infrastructure depends on it: - **Search crawlers** follow every link they discover, including query strings. - **Browser prefetch and preload** (`<link rel=prefetch>`, speculation rules, address-bar preloading) fetch URLs before the user has decided to visit them. - **Link-preview fetchers** in chat, mail and social apps fetch a URL the moment someone pastes it. - **Security scanners and anti-virus tools** crawl links in incoming mail to check for malware. - **Caching proxies and revalidators** re-issue GETs on their own schedule. None of these ask permission, because HTTP told them GET is read-only. ## What breaks when a GET mutates The classic failure has happened repeatedly in the real world: an internal admin tool exposed actions as links — `GET /admin/users/42/delete` — and a crawler, an accelerator plugin, or a mail scanner walked them all. The result is bulk data destruction with no user having clicked anything, and no attacker involved. Other recurring shapes: - **One-click unsubscribe via GET** fired by a mail client's link scanner, unsubscribing users who never opened the mail. (This is why the standardized one-click unsubscribe flow uses POST.) - **`GET /cart/add?sku=…`** re-run by a prefetcher, filling carts on hover. - **Prefetch amplification:** the browser prefetches a link the user never clicks, and your "view" mutation runs anyway. - **Retry amplification:** because GET is idempotent, client libraries and proxies auto-retry it on connection failure — so the mutation runs an unpredictable number of times. - **Caching confusion:** a GET response may be cached by a shared cache, so subsequent "requests" never reach you at all and the mutation stops happening — the reverse failure, equally confusing to debug. Note the compounding: violating safety also drags in idempotency and cacheability assumptions, because safe methods are the ones intermediaries feel free to retry *and* cache. ## What is explicitly allowed The spec anticipates that servers do things while serving a GET, and permits it: - Writing access logs. - Incrementing a page-view or article-read counter. - Updating a "last accessed" timestamp. - Warming or populating a cache. - Recording rate-limit consumption. - Creating a session on first contact. All of these are the server's own bookkeeping, not something the client requested, so the accountability test is satisfied. The line is: **did the client ask for this change, or did the server decide to do it?** If a user would reasonably say "I only asked to look at the page", you are still inside safety. The pragmatic caveat is that even permitted effects should tolerate the traffic patterns safety invites. If a view counter is incremented by every crawler and prefetcher, the number is noise; if the counter write is expensive, prefetch traffic becomes a load problem. That is an engineering concern, not a spec violation. ## HEAD and OPTIONS HEAD must be identical to GET minus the body, so anything unsafe in your GET handler is unsafe in HEAD too — and monitoring systems love HEAD. OPTIONS is used for CORS preflight and is issued automatically by browsers, so an OPTIONS handler that mutates would be triggered by ordinary cross-origin traffic. ## The fix and how to talk about it Move the mutation to a method whose contract admits state change: POST for "process this", PUT for replacement, PATCH for partial update, DELETE for removal. In HTML, that means a form rather than an anchor. In an API, it means resisting the temptation to make an action "easy to trigger from a browser address bar" — that convenience is exactly what the crawler will use. A strong answer distinguishes the two halves clearly: safety is about **requested** change and accountability, so incidental server bookkeeping is fine; and safety matters because unknown third-party software will exercise every GET it can find.
- Is incrementing a page-view counter on GET a violation of safety?No. The user asked to view the page, not to change the counter, so under RFC 9110 they cannot be held accountable for the increment — it is server-side bookkeeping. The practical caveat is that crawlers and prefetchers will inflate the number and add write load, so treat the count as approximate and make the write cheap.
- How does violating safety also break retry behavior?Safe methods are idempotent, so HTTP clients, connection pools and proxies automatically retry GETs when a response is lost. A mutating GET therefore runs an unpredictable number of times on any flaky connection, on top of being triggered by crawlers. One safety violation drags in an idempotency violation.
- Why do standardized one-click unsubscribe flows use POST rather than a plain link?Because mail clients, spam filters and security scanners fetch the links in a message to check them, and a GET-based unsubscribe would fire for users who never interacted with the mail. Requiring POST means only a deliberate action — a form submission — triggers the state change.
Safety is the difference between reading a price tag and moving the item to the till. The shop still records that you looked — that is the shop's bookkeeping, not your purchase.
saying these in an interview costs you the question
- Claiming any server-side write during a GET (logs, counters) violates safety.
- Defending GET-based mutations because 'the endpoint requires authentication' — crawlers inside a logged-in session and prefetchers still reach it.
- Thinking safety is enforced by browsers or the protocol rather than being a contract you can break.
- Not connecting safety to crawlers, prefetchers and link-preview fetchers.
- Forgetting that HEAD must mirror GET, so an unsafe GET handler is unsafe over HEAD too.