GraphQL path · In production · Topic 8 of 12

Caching behind one endpoint

Not started

Caching GraphQL is harder than caching REST for one structural reason: every request goes to the same URL, and the question is in the body. Browser caches and CDNs key on method and URL and don't reuse POST responses, so out of the box they cache nothing. GraphQL is not uncacheable, though; it is as cacheable as any API with parameters. The caching just happens in different places: in the URL (GET requests with persisted query hashes), in the client (a normalised store of objects by identity), and on the server (loaders and data caches below the resolvers).

Why it matters: REST gets CDN and browser caching almost for free because every resource has a URL. GraphQL sends different questions to one URL, usually as POST, so caching moves into persisted queries, the client and the server's data layer.

  1. 1One URL, many answers

    HTTP caches key on method and URL. A POST to `/graphql` with a body is not cached by browsers or CDNs at all.

  2. 2GET plus persisted queries

    Send queries as GET with a document hash instead of the full text. The URL becomes a stable, short cache key, and `Cache-Control` works again.

  3. 3Normalised client caches

    Clients such as Apollo, Relay and urql store each object once by type and id. A mutation that returns the updated object refreshes every screen that shows it.

  4. 4Cache data, not responses

    On the server, the reliable win is caching what resolvers load (per request with loaders, across requests with an application cache), whatever query asked for it.

1

How it works

The explanation, with examples you can run.

Making HTTP caching work again

The GraphQL over HTTP draft allows GET for queries, with the document and variables in the query string. Long documents make long URLs, so most setups use persisted queries: the client sends a hash of a document the server already knows.

GET /graphql?extensions={"persistedQuery":{"sha256Hash":"9f2c..."}}&variables={"id":"p1"}Cache-Control: public, max-age=60

Now the URL is short and stable per query and variables, which is a key a CDN can use. When the documents are registered at build time and only those are accepted, they are called trusted documents and also serve as an allowlist (see the security topic). The response's Cache-Control must reflect its most sensitive field: if any part is per-user, the whole response is private. Some servers let you annotate fields with cache hints and compute the header from the fields actually selected.

The client's normalised cache

⌘/Ctrl + Enter
OutputNot run yet

Press Run to see what your code prints, in order.

Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.

This is the idea behind the caches in Apollo Client, Relay and urql's graphcache: split responses into objects keyed by type and id, store each once, and rebuild query results from references. It only works if the schema gives objects stable identities, which is why graphql.org recommends a unique id on object types. The hard cases are lists: after createPost, the cache can't know which lists the new post belongs in, so the client updates them explicitly or refetches.

On the server

Whole-response caches on the server rarely pay off, because two clients seldom send exactly the same query and variables. What does pay off is caching below the resolvers: DataLoader de-duplicates within a request, and an application cache (Redis, an in-memory LRU) in front of slow services helps every query that touches the same data, whatever its shape.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.Why does a CDN in front of `/graphql` usually cache nothing?
  2. 2.What makes normalised caching possible on the client?
  3. 3.One response mixes public product data and the viewer's cart. What `Cache-Control` should it get?

0 of 3 right so far. Get all three to finish this topic.