For a read-heavy public catalogue API, why does HTTP caching favour resource-style GET requests over RPC or GraphQL calls sent as POST?
answer
- what a cache keys on
- safe method, cacheable response
- the operation hidden in the body
- POST cacheable only on strict terms
basics
~20 sShared HTTP caches reuse responses to safe GET requests, keyed on the URL, so per-item catalogue URLs are served from browsers, proxies and CDNs. RPC or GraphQL POSTs to one endpoint hide the operation in the body, defeating those caches.
solid answer
~50 sRFC 9110 defines `GET` as **safe** and its response as **cacheable**, and a cache identifies a stored response by the request's target URI (plus headers named in `Vary`). A resource-style API that puts each product at `/products/42` therefore gets browser caching, shared proxy and CDN caching, and conditional revalidation with `ETag` / `If-None-Match` and `304 Not Modified` for free. An RPC or GraphQL call carried as `POST` to a single endpoint names the operation in the body: RFC 9110 lets a `POST` response be cached only with explicit freshness and a `Content-Location` equal to the target URI, it can then serve only a later `GET` or `HEAD`, and the RFC notes most caches support only `GET` and `HEAD` anyway. So RPC and GraphQL APIs move caching into the client or the server application, or map side-effect-free operations onto `GET`.
go deeper
Recall that GET is safe and cacheable and keyed on the URL, while a POST to one endpoint hides the operation from caches.
Explain the RFC 9110 rules: why a POST response is cacheable only with freshness and a matching Content-Location, and how ETag revalidation saves bandwidth.
Judge how much traffic is really cacheable before choosing a style, and know the workarounds - GET mappings, query-as-URL, client stores - and their limits.
Weigh offloading reads onto the web's caching infrastructure against the contract and flexibility gains of RPC or GraphQL for each consumer group.
## What an HTTP cache needs An HTTP cache - in the browser, in a forward proxy, in a reverse proxy or a CDN in front of the origin - can only reuse a response when two things are true: - the **method** allows caching and says when a stored response may be reused, and - the **request identifies what it asks for** in parts the cache can key on: the target URI, plus any request headers the response lists in `Vary`. RFC 9110 (HTTP Semantics, §9.2.3) states that a method definition must explicitly allow caching for a response to be stored, defines caching semantics for `GET`, `HEAD` and `POST`, and adds that "the overwhelming majority of cache implementations only support GET and HEAD". ## Why resource-style GET fits 1. **`GET` is safe** (RFC 9110 §9.2.1): the client asks for no state change, so a cache can answer on the origin's behalf without harm. 2. **Its response is cacheable** (§9.3.1): a cache MAY use it for later `GET` and `HEAD` requests unless `Cache-Control` (from the HTTP caching specification) says otherwise. 3. **The URL names the thing**: `/products/42` and `/products/43` are different entries, so one product's change does not disturb another's cached copy. 4. **Revalidation is cheap**: the server sends an `ETag`; a client or cache later sends `If-None-Match`, and the origin answers `304 Not Modified` without a body when nothing changed (§13.1.2, §15.4.5). 5. **Variants stay explicit**: when a representation depends on a request header such as `Accept-Language`, the origin lists it in `Vary` (§12.5.5) and the cache keeps one entry per variant. For a public catalogue read thousands of times per write, that machinery offloads most traffic from the origin without any API-specific code. ## Why RPC and GraphQL over POST do not An RPC framework or a GraphQL server carried over HTTP typically sends every call as `POST` - gRPC defines every call's method as `POST`, and GraphQL clients commonly `POST` the query document to one URL. Three things go wrong for a cache: - **The operation is in the body.** `GetProduct(42)` and `GetProduct(43)` share a URL; a cache keyed on the URL cannot tell them apart. - **`POST` is not safe.** A cache cannot assume that replaying a stored answer is harmless, and RFC 9110 §9.3.3 says a `POST` request "cannot be satisfied by a cached POST response". - **`POST` responses are cacheable only on strict terms.** They need explicit freshness and a `Content-Location` equal to the `POST`'s target URI, and even then can only satisfy a later `GET` or `HEAD`. | | REST `GET /products/42` | RPC `POST /catalog.GetProduct` | GraphQL `POST /graphql` | |---|---|---|---| | Method safe | yes | no | no | | Distinct cache key per item | the URL | no, item id is in the body | no, query is in the body | | Shared cache reuse | yes | effectively none | effectively none | | Conditional revalidation with `ETag` | built in | only if built by hand | only if built by hand | ## What RPC and GraphQL designs do instead - **Map reads onto `GET`.** Some RPC systems let a side-effect-free method be exposed as a `GET` on a URL template, which restores HTTP caching for that method. GraphQL queries can also be sent with `GET`, the query in the URL; cacheable, but every distinct query text becomes its own cache entry, so hit rates depend on clients sending identical queries. - **Cache in the client.** GraphQL clients commonly keep a normalised store of entities by type and id; RPC clients keep application-level caches. These help one client, not every client behind a shared proxy. - **Cache in the server.** Response or data caches inside the service cut database load but still cost a full request to reach. ## The judgement HTTP caching is one input among several. If the surface is a **public, read-dominated, URL-shareable** catalogue, resource-style `GET`s let the web's existing caches carry the load and the trade-off favours REST strongly. If the reads are personalised, change on every request, or happen between internal services that never pass through a shared cache, the loss is small and the other strengths of RPC or GraphQL - generated contracts, client-chosen fields - can win. Measure what share of traffic is cacheable before letting caching decide.
- Does sending GraphQL queries with GET solve the caching problem completely?Partly. A `GET` with the query in the URL is safe and cacheable, so shared caches can store it, but the cache key is the whole URL: two clients asking for the same data with differently written queries create two entries. Hit rates depend on clients sending identical query text, and long query strings can run into URL length limits.
- When can a POST response be cached under RFC 9110, and what can it be reused for?Only when it carries explicit freshness information and a `Content-Location` header whose value equals the POST's target URI. Even then the stored response can satisfy a later `GET` or `HEAD` for that URI, never a later `POST`, because `POST` is potentially unsafe. RFC 9110 also notes most caches implement only `GET` and `HEAD` caching.
saying these in an interview costs you the question
- POST responses can never be cached under HTTP's rules.
- A CDN caches any response as long as it has a Cache-Control header, whatever the method.
- An RPC API gets HTTP caching for free because it runs over HTTP.
- GraphQL over GET caches as well as REST because the method is the same.
- Caching is only a performance tweak and never a reason to pick a style.