Idempotency vs safety in HTTP — what SA interviewers test
Contents:
Why this pair shows up in every SA loop
Almost every systems analyst interview at Stripe, Uber, DoorDash, or any payments-adjacent shop opens with some flavor of: "What's the difference between an idempotent and a safe HTTP method, and which one is GET?" It looks like a definitions question. It is not. The interviewer is checking whether you understand two orthogonal properties of HTTP methods, whether you can map them onto retry semantics, and whether you can spot the anti-pattern of action-on-GET in someone else's API design.
Get this wrong and you'll spend the rest of the loop on the back foot, because half of the follow-up system-design questions — payment retries, webhook delivery, background job queues, double-charge prevention — sit on top of these definitions. The good news: once you internalize that safe and idempotent are two separate axes, not synonyms, the rest collapses into a memorizable table.
Load-bearing trick: safety is about whether the server's state changes at all. Idempotency is about whether repeated calls converge to the same final state. A method can be idempotent without being safe — PUT and DELETE both modify state, but applying them twice leaves the system in the same place as applying them once.
The two definitions, precisely
Safe. A method is safe if it does not modify server state in any observable way. Read-only. The canonical safe methods are GET, HEAD, and OPTIONS. Logging the request, incrementing a counter, or caching the response do not break safety because the resource itself is untouched. RFC 9110 §9.2.1 is the source of truth here.
Idempotent. A method is idempotent if making the same request N times produces the same server-side effect as making it once. The idempotent methods are GET, HEAD, OPTIONS, PUT, and DELETE. Note that the response body or status code can differ between calls — DELETE returns 200/204 the first time and 404 the second — but the final state of the resource is identical. That distinction trips up at least one candidate per loop.
Cacheable. Separate property, often bundled into the same question. GET and HEAD are cacheable by default. POST is cacheable only when the response includes explicit Cache-Control or Expires headers, and almost nobody relies on this in practice.
The relationship is one-directional: every safe method is idempotent, but not every idempotent method is safe. PUT is idempotent but unsafe. GET is both.
The HTTP methods matrix
This is the table you should be able to draw on a whiteboard inside thirty seconds. Memorize the shape, not the cells.
| Method | Safe | Idempotent | Cacheable | Typical use |
|---|---|---|---|---|
| GET | yes | yes | yes | Fetch a resource |
| HEAD | yes | yes | yes | Fetch headers only |
| OPTIONS | yes | yes | no | CORS preflight, capability discovery |
| PUT | no | yes | no | Replace resource at known URI |
| DELETE | no | yes | no | Remove a resource |
| POST | no | no | rarely | Create a resource, run a side effect |
| PATCH | no | depends | no | Partial update |
The two cells interviewers will drill into are the POST row and the PATCH row. POST is the only "common" method that is neither safe nor idempotent by default — which is precisely why payment APIs invented the Idempotency-Key header to bolt idempotency onto POST. PATCH is the squishy one: a PATCH that sets a field to a value is idempotent; a PATCH that increments a counter is not.
Sanity check before you answer: if the interviewer asks "is PATCH idempotent?", the correct answer starts with "it depends on the payload semantics", not yes or no. Anything else loses points.
Retries, proxies, and crawlers
The reason all of this matters in practice is what clients are allowed to do without asking you. Three actors silently rely on these properties, and your API design has to respect their assumptions.
Browsers and HTTP proxies treat GET as safe by contract. They will prefetch links, replay GETs on back-button navigation, and let intermediate proxies cache responses. An API that exposes GET /delete-user?id=42 will eventually be triggered by a corporate proxy's link-validation crawler, a Slack URL unfurler, or a user's "Open all in new tabs" — and you will get a Sev-1 about phantom deletions. The fix is mechanical: any state-changing endpoint becomes POST, PUT, PATCH, or DELETE.
Anti-pattern: GET /api/v1/users/delete?id=42
Correct: DELETE /api/v1/users/42HTTP clients and SDKs retry idempotent methods automatically on network failures (connection reset, 502, 503, 504). Stripe's stripe-node, Google's client libraries, and the AWS SDK all do this. They will not automatically retry POST without an explicit idempotency key, because retrying a non-idempotent POST risks duplicate charges, duplicate orders, duplicate emails. This is why every payments API ships an Idempotency-Key header.
Search crawlers and link-preview bots fetch URLs with GET, and they do it from IPs you cannot predict. If your "unsubscribe" link is a GET that mutates state, every link checker that scrapes the email body will unsubscribe the user. Use a one-time token plus a POST confirmation page, or accept that your unsubscribe metric is permanently inflated.
The whole point of the safe/idempotent contract is that intermediaries can act on it without coordinating with you. Break the contract and you break the assumption every piece of HTTP infrastructure depends on.
Common pitfalls
When candidates fumble this question, it's almost always one of five traps. The interviewer is fishing for these — bringing them up unprompted is the fastest way to signal seniority.
The first pitfall is conflating idempotent with deterministic response. DELETE is idempotent: deleting the same resource twice leaves it deleted. But the first call returns 200 or 204, and the second returns 404 because the resource is already gone. Candidates who insist "DELETE must return the same status code every time, otherwise it's not idempotent" are mixing up server state with response shape. RFC 9110 defines idempotency over the effect on the server, not the bytes coming back.
The second pitfall is assuming POST is forever non-idempotent. POST is non-idempotent by default, but you can layer idempotency on top using a client-supplied key — the header is conventionally named Idempotency-Key and Stripe popularized the pattern. The server stores the key for some retention window (24 hours is typical, Stripe holds them for 24 hours) and short-circuits subsequent requests with the same key to return the cached response. The endpoint becomes idempotent from the client's perspective even though the HTTP method itself is not.
The third pitfall is treating PUT as a generic update. PUT in RFC semantics is a full-resource replacement at a known URI: PUT /users/42 {name: "Alice", email: "a@x.com"} replaces the entire resource. If you send only {name: "Alice"}, you risk wiping the email field, which means your "PUT" is actually a malformed partial update that should have been PATCH. The idempotency guarantee of PUT depends on this replacement semantics — sending the same complete body N times yields the same state.
The fourth pitfall is race conditions inside an idempotent operation. Two concurrent PUTs with different bodies will both succeed, and the last one wins — that's still idempotent in the formal sense (replaying either one gives the same result), but it's a lost-update bug in disguise. The interview-grade answer is to mention optimistic concurrency control via If-Match headers and ETags, which lets the second PUT fail with 412 Precondition Failed instead of silently overwriting.
The fifth pitfall is forgetting that idempotency is a server-side promise, not a client claim. A client cannot decide a POST is idempotent just because it feels nice. The server must implement the deduplication — usually a unique constraint on (idempotency_key, endpoint), a write-through cache of completed responses, and a state machine that distinguishes "in flight" from "completed" so a retry mid-flight returns the right thing.
Related reading
- HTTP methods and status codes for systems analysts
- Idempotency keys for systems analysts
- API contract testing for systems analysts
- Authentication vs authorization on the SA interview
If you want to drill systems analyst questions like this every day, NAILDD is launching with hundreds of API-design and HTTP-semantics problems modeled on real loops at Stripe, Uber, and DoorDash.
FAQ
Is GET always safe in practice?
Formally, yes — RFC 9110 defines GET as safe and idempotent. In practice, plenty of legacy APIs ship "GET /trigger-export" endpoints that kick off background jobs, which is a violation. The right way to express the same intent is a POST that returns 202 Accepted with a job ID and a polling URL. If you inherit an unsafe GET in a code review, flag it: every crawler, browser extension, and corporate proxy will eventually trigger it.
What's the difference between PUT and PATCH in interview terms?
PUT replaces the entire resource at a URI; PATCH applies a partial modification. PUT is always idempotent because replaying the same full body yields the same state. PATCH is idempotent only when the payload describes a target state ({status: "active"}) rather than a delta ({balance: +10}). On the interview, the crisp framing is: PUT is set, PATCH is apply, and apply may or may not be idempotent depending on whether it's a set or a delta.
How does the Idempotency-Key header actually work?
The client generates a UUID per logical operation (one charge attempt, regardless of retries), sends it in the Idempotency-Key HTTP header, and the server stores (key, response_hash, response_body, status) for a retention window. On a duplicate request with the same key, the server returns the cached response without re-executing the side effect. The atomic guarantee comes from a unique constraint in the database — usually on (idempotency_key, merchant_id) — that fires before the charge is actually processed.
What status code should DELETE return on a missing resource?
Two valid choices: 404 Not Found if you want to be honest about the current state, or 204 No Content if you want to be strictly idempotent-friendly and avoid leaking whether the resource ever existed. Most public APIs (Stripe, GitHub) return 404 on the second DELETE, which is the more common convention. Returning 204 both times is fine too — what matters is that you've made the choice intentionally and documented it.
Why is OPTIONS not cacheable?
OPTIONS is used primarily for CORS preflight, where the response describes which methods, headers, and origins are allowed for a given resource. The CORS specification has its own cache mechanism via the Access-Control-Max-Age response header, which lets browsers cache the preflight result for a configured window. Standard HTTP caching is bypassed because the semantics — capability discovery — don't match what generic HTTP caches are built for.
Can you have an idempotent POST without a key?
Yes, if the operation is naturally idempotent — for example, POST /users/42/subscribe-to-newsletter where subscribing twice is the same as subscribing once. The server enforces the no-op via a unique constraint on (user_id, newsletter_id). This is cleaner than retrofitting an Idempotency-Key header when the resource model already gives you uniqueness. The general rule: if the operation has a natural primary key in the request, use that; otherwise, require an explicit idempotency key from the client.