Why can't an HTTP cache reuse a GraphQL response when every operation is POSTed to one URL?
answer
- One door, many different requests
- What is the lookup key made of
- The distinguishing part travels in the body
- Method plus URI, narrowed by Vary
- Caches never parse a request body
basics
~20 sHTTP caches key stored responses on the request method and target URL. Every GraphQL operation is POSTed to the same path, so the discriminator — the document and its variables — sits in a body no cache reads.
solid answer
~50 sAn HTTP cache is a lookup table whose key is the request method plus the target URI, narrowed by whatever the response's `Vary` header names. A GraphQL client sends every operation as a POST to a single path, so two completely unrelated requests — one asking a solar-array telemetry graph for a site's inverter list, one asking for a single panel string's output history — present an identical key. The discriminator is the request **body**: the executable document and its variables. Bodies are not part of the HTTP cache key, and a shared cache will not parse GraphQL to build one. So a URL-keyed cache has only two honest options, and it takes the safe one: store nothing. The information needed to cache correctly exists, but it is on the wrong side of the HTTP boundary. Getting it back means moving the operation into the URL, which is what queries over GET and persisted identifiers are for.
code
json · 4 lines{
"query": "query SiteInverters($site: ID!) { site(id: $site) { inverters { id model } } }",
"variables": { "site": "AZ-14" }
}go deeper
Be ready to state the cache key out loud: method plus URL. Then say where GraphQL puts the thing that makes two requests different — the body — and why that combination leaves a cache nothing to key on.
Explain why Vary cannot rescue this, since it selects on request headers only, and name the three losses concretely: shared caches, the browser's private store, and conditional revalidation.
Frame it as a deliberate trade rather than a flaw, and be able to say when the loss actually costs money — public read-heavy anonymous traffic — versus when it was never available anyway, as with per-viewer responses.
Own the argument that adopting a single-endpoint API moves caching, observability and rate limiting from infrastructure your ops team already runs into application code your team must build and staff.
## What an HTTP cache actually keys on Every HTTP cache — the browser's private store, a corporate forward proxy, a reverse proxy in your own datacentre, a CDN edge node — is a lookup table. The primary key is the **request method plus the target URI**. The response can narrow that key by listing request header names in `Vary`, so `Vary: Accept-Encoding` splits the entry into a gzip variant and an identity variant. That is the entire key space. Nothing in it is derived from the request body, and this is deliberate: the caching rules were written around a method whose semantics are "fetch the thing this URI names", where the URI *is* the identity of the thing. REST leans on that. `GET /arrays/AZ-14/inverters` names a resource; a cache can store the bytes under that URI and answer later readers without the origin ever hearing about them. Change the resource, change the URI, and the two entries never collide. ## Where GraphQL puts the discriminator GraphQL deliberately abandons resource URLs. There is one endpoint — conventionally something like `/graphql`, though the specification does not mandate a path — and the request body carries the executable document, the variables and optionally an operation name. That body is the *only* thing distinguishing one request from another. Take a solar-array telemetry graph. One screen sends: ```json {"query":"query SiteInverters($site:ID!){ site(id:$site){ inverters { id model } } }", "variables":{"site":"AZ-14"}} ``` Another sends: ```json {"query":"query StringOutput($s:ID!){ panelString(id:$s){ powerOutputKw lastReadingAt } }", "variables":{"s":"AZ-14-S07"}} ``` To the application these are unrelated reads of different data with different volatility. To a cache sitting between them they are the same key: `POST https://api.example.com/graphql`. The two responses have nothing in common, so an entry stored for the first would be a *wrong answer* to the second. A cache that served it would be broken, not merely inefficient. ## The three things this breaks **Shared caching is gone.** A CDN or reverse proxy in front of the endpoint sees one hot URI with wildly varying content and no safe way to split it. Every request travels to the origin. If ten thousand dashboards all ask for the same site summary in the same minute, the origin answers ten thousand times. **The browser cache is gone too.** The private cache in the user's browser follows the same rules; a POST result is not stored for reuse, so navigating back to a screen re-issues the request over the network. **Revalidation is gone.** Conditional requests are cheap because the client already holds a stored copy and a validator for it, and asks only "is this still current?". With nothing stored under the URI there is nothing to revalidate, so the endpoint pays full price for every read, including reads whose answer has not changed in hours. ## This is a consequence, not a bug It is worth saying plainly in an interview: nothing here is a defect in GraphQL, and nothing here is a defect in HTTP. It is the bill for a design trade. GraphQL bought precise, client-shaped responses and the removal of over- and under-fetching by giving up the property HTTP caching is built on — that the URI identifies the response. You cannot keep both for free. The volume of the bill depends on the traffic. An internal dashboard behind a login was never going to get shared-cache hits anyway, because its responses are per-viewer and a shared cache could not store them regardless of transport. A public, read-heavy, largely anonymous API is where the loss really hurts, and it is exactly where teams end up reaching for a way to put the operation back in the URL. ## What is still true The body being invisible to HTTP does not mean nothing anywhere caches. It means the *HTTP* tier cannot, and so caching migrates: into the client's own store, into the server's parsed-document and response caches, into the data layer behind the resolvers. Those tiers are real and often large. They are simply not addressable with a URL, not visible in a proxy's hit-rate metric, and not purgeable by the tooling an operations team already owns — which is the second half of the problem this endpoint shape creates. The recovery path, at a high level, is to make the operation part of the request line again: send the read as a GET whose query string carries the document (or a short identifier standing in for it) plus the variables, so the URI once more names the answer. That reopens the door — for reads only, and only for responses that are not per-viewer.
- Would adding `Vary` to the response let a shared cache tell two GraphQL operations apart?No. `Vary` narrows the key by naming **request header** fields, so it can split an entry by encoding, language or an auth-related header. It has no way to reference the request body, and the document plus variables live only in the body. You could smuggle an operation identifier into a custom request header and vary on it, but at that point you have invented a worse URL: harder to inspect, harder to purge, and unsupported by most edge configurations.
- Does this problem disappear for a private, per-user API where shared caching was impossible anyway?The shared-cache loss is indeed nil — a per-viewer response could never be stored in a CDN whatever the transport. What you still lose is the **browser's private cache** and conditional revalidation, so repeat reads by the same user always hit the origin. That is why private APIs feel the pain less and usually solve it with a client-side store rather than anything at the HTTP layer.
- If every operation shares one URL, how do you tell them apart in access logs and latency metrics?You cannot, from the request line alone — one URI, one method, and per-endpoint p99 that averages a trivial lookup with an expensive report. Teams recover the dimension inside the application by tagging each request with the operation name and reporting metrics per operation. It is the same root cause as the caching problem: the identity of the request is in the body, so anything keyed on the URL loses it.
A URL-keyed cache is a coat check that files tickets by the door you came in through. GraphQL sends everyone through one door and writes what they actually want on a slip inside their pocket — the attendant never opens it, so no two tickets can be told apart.
saying these in an interview costs you the question
- Claims a cache can key on the POST body
- Says GraphQL responses are simply never cacheable anywhere
- Thinks Vary can select on request body content
- Confuses this with responses being marked no-store
- Believes only mutations are uncacheable, queries are fine
- Says the browser cache still stores POST results for reuse