Under HTTP, when may a cache store a response to a POST, and why doesn't that help a GraphQL endpoint?
answer
- POST is conditional, not forbidden
- Two conditions, both must hold
- One header ties the payload to a URI
- The entry answers a different method
- Unsafe requests clear the entry
basics
~20 sHTTP permits storing a POST response only when it carries explicit freshness and a Content-Location equal to the POST's target URI, and the entry then serves only a later GET or HEAD. One GraphQL endpoint gains nothing.
solid answer
~50 sPOST is not an uncacheable method in HTTP — it is a **conditionally** cacheable one. The rule is narrow: the response must include explicit freshness information, and it must carry a `Content-Location` whose value is the POST's own target URI. Only then may a cache store it, and the stored entry is usable to satisfy a later **GET or HEAD** on that URI — never a later POST. On a single GraphQL endpoint both halves fail. Every operation targets the same URI, so one stored entry would answer for all of them, which is exactly the collision that makes the entry wrong. And the follow-up traffic is more POSTs, not GETs, so even a correctly stored entry would never be consulted. There is a third effect that runs the other way: a successful response to an unsafe request invalidates stored responses for that URI, so each POST actively clears the endpoint's entry.
code
json · 11 linesPOST /graphql HTTP/1.1
Content-Type: application/json
{"query":"query { site(id:\"AZ-14\"){ inverters { id } } }"}
HTTP/1.1 200 OK
Cache-Control: max-age=300
Content-Location: /graphql
Content-Type: application/graphql-response+json
{"data":{"site":{"inverters":[{"id":"INV-3"}]}}}go deeper
You are not expected to recite this rule. Know only that in practice POST responses are not served from caches, and that the working fix is putting the read into a URL rather than adding a header.
Be able to name both conditions — explicit freshness and a matching Content-Location — and say that the resulting entry serves a GET or HEAD, not another POST. That is the layer beneath the usual shorthand.
Use this to kill the plausible-but-wrong suggestion of slapping a lifetime on the POST response, and to explain why sharing one path between cacheable reads and mutations is fragile given unsafe-request invalidation.
Own the framing: protocol-legal is not the same as deployable. Shared caches broadly do not implement POST storage, so a design that depends on it is a design that depends on infrastructure you do not control.
## POST is conditionally cacheable, not uncacheable The common shorthand — "you can't cache a POST" — is wrong as a statement about the protocol and right as a statement about practice. HTTP defines POST as cacheable under conditions, and the conditions are strict enough that almost nothing meets them. A cache may store the response to a POST only when **both** of the following hold: 1. The response carries **explicit freshness information** — a lifetime the origin stated outright, rather than one a cache heuristically guessed. Heuristic freshness, the trick a cache uses when a response has no stated lifetime, is not permitted here. 2. The response carries a **`Content-Location`** header field whose value is the **same URI the POST was sent to**. The second condition is the interesting one, and its purpose is precise. `Content-Location` says "the payload you are holding is a representation of *this* URI". Without it, the response to a POST is the outcome of an action, not a representation of anything, and a cache has no honest URI to file it under. With it, the origin is asserting that a GET on that same URI would return this. So the stored entry is allowed to satisfy a subsequent **GET or HEAD** on that URI. Note what is *not* permitted. The entry cannot answer a later POST. A cache reuses a stored response only for a request whose method the entry is valid for, and there is no mechanism by which two POSTs — which may carry entirely different bodies — are treated as the same request. The rule exists to let a POST **warm** a GET-addressable resource, not to make POST behave like GET. ## Now apply it to one GraphQL endpoint Suppose a solar-array telemetry service tried to satisfy the letter of the rule. It answers a POST to `/graphql` with a stated lifetime and `Content-Location: /graphql`. The first condition is satisfiable. The second is technically satisfiable and semantically a lie: the service is asserting that a GET on `/graphql` returns this site-inverter payload, when the same URI is simultaneously the address of every other operation the schema exposes. The moment a second dashboard POSTs a panel-string query, the same URI is asserted to be a different document. One key, two irreconcilable representations. And even if you accepted the lie, the payoff is zero, because of who reads the entry. A stored POST response answers **GETs**. The traffic on a POST-only GraphQL endpoint is more POSTs. The clients that would benefit never present a request the entry is allowed to satisfy. ## The effect that runs backwards There is a third piece worth knowing, because it surprises people who assume the endpoint is merely cache-neutral. A cache is required to **invalidate** stored responses for the target URI when it forwards an unsafe request there and gets back a non-error response. POST is unsafe. So every successful GraphQL POST that passes through a shared cache clears whatever that cache was holding under the endpoint URI. On its own that changes nothing, since there was no useful entry. But it is why a hybrid scheme — reads over GET so they can be stored, writes over POST to the same path — has a footgun: the mutation traffic invalidates the endpoint's entry as a side effect of the method's semantics. It is a reason people give mutations a distinct path, or accept that the read entries are keyed by full query strings that the mutation's bare URI does not match. ## Why this shows up in interviews at all It is a differentiator, not a gate. A strong candidate who says "POST responses aren't cached" is right about every deployment they will ever operate: shared caches overwhelmingly do not implement POST storage, and configuring one to try would be a strange choice. Knowing the actual rule matters for two reasons. It stops you from proposing "just send `Cache-Control: max-age=300` on the POST response" as a fix — a plausible-sounding suggestion that buys nothing, because the freshness half of the rule is the half that was never the problem. And it makes the real fix obvious for the right reason: the missing ingredient is not a header, it is a **URI that names the answer**, which is precisely what moving the operation into the request line supplies. ## Answering it cleanly State the two conditions, state that the entry serves only GET or HEAD, then give the two-part reason it is useless here — one URI cannot honestly represent every operation, and the traffic is POSTs which the entry may not answer. Mentioning the invalidation effect is the detail that shows you have read the rule rather than heard it summarised.
- What is `Content-Location` actually asserting, and why does the caching rule hinge on it?It asserts that the enclosed payload is a representation of the URI it names — that a GET on that URI would yield this. Caching needs exactly that assertion, because a cache files responses under a URI and must believe the URI identifies the content. The response to a POST is normally the *result of an action* rather than a representation of anything, which is why, without this header, there is no honest place to file it.
- Would sending explicit freshness on the POST response alone gain anything?No. Freshness is the condition that was never the obstacle. Without a matching `Content-Location` a cache may not store the response at all, and even with one it may only reuse the entry for a GET or HEAD, which a POST-only client never sends. The absent ingredient is a URI that identifies the answer, not a lifetime.
- If reads move to GET on the same path while mutations stay POST, what does the POST traffic do to the stored entries?A non-error response to an unsafe request obliges a cache to invalidate stored responses for that target URI. In practice the read entries are keyed by the full request target including the query string, so a bare mutation URI does not match them and they survive; but it is fragile reasoning to rely on, and it is one reason teams give mutations their own path rather than sharing one with cacheable reads.
saying these in an interview costs you the question
- Says HTTP forbids caching POST responses outright
- Thinks a max-age header alone makes a POST cacheable
- Believes the stored entry can answer later POSTs
- Confuses Content-Location with the Location header
- Ignores that unsafe requests invalidate stored entries