skip to content

Caching & Persisted Queries

GraphQL gives up ordinary HTTP caching the moment every operation goes through one POST endpoint, and this is what you get back. Interviewers ask for the whole ladder, edge to client store.

part ofGraphQLoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

Why does returning the updated object from a GraphQL mutation refresh a normalized client cache?

level: juniorimportance: must knowfreq 68%

answer

  1. The store is keyed by identity
  2. A mutation response is shredded like any other
  3. Same id, new field values, merged in
  4. Unselected fields are not refreshed
  5. Values, never list membership

basics

~20 s

A normalized store keys objects by type and id, not by the operation that fetched them. When a mutation's response carries the same id with new field values, the store overwrites that entity, and every view reading it updates.

solid answer

~50 s

A normalized client cache shreds every response into entities addressed by identity - conventionally the type name plus the object's `id` - so a value written by one operation is the same value read by all the others. A mutation response goes through exactly the same shredding. If the payload selects the changed object's `id` **and** the fields whose values changed, the store merges those fields onto the existing entity and notifies every active operation that reads it. Nothing re-runs against the server. Two consequences follow. Fields the payload did not select are not refreshed - they keep whatever the store already held, which is why a partial payload silently leaves stale values behind. And it only works for **value** changes to an object the store already holds by that identity: it cannot tell any list that its membership changed.

code

graphql · 10 lines
graphql
mutation ShiftDoors($id: ID!, $at: DateTime!) {
  updateEventDoorsOpenAt(eventId: $id, doorsOpenAt: $at) {
    event {
      id
      doorsOpenAt
      salesCloseAt
      updatedAt
    }
  }
}

go deeper

for a junior

Recall the one-line reason: the cache stores objects by identity, so a mutation returning the same id with new values updates every screen reading that object. Be ready to say why the payload must select id.

for a middle

Explain the merge precisely - only the fields the payload selected are written, the rest of the entity is untouched - and name the three failure modes: missing id, an unselected changed field, and a change that landed on a different entity.

for a senior

Show that you treat the payload selection as the invalidation contract for the write, and that you know its hard limit: values only, never membership. Be ready to say what you escalate to and what that costs in round trips.

for a principal

Own the tradeoff between the cheapest correct mechanism and blanket refetching, and the fact that identity-keyed caching is a client convention the schema has to cooperate with - a stable identifier on every mutable type is a platform decision, not a per-feature one.

## The mechanism, stated precisely A normalized client cache does not store responses. It shreds each response into individual objects and stores them in a flat map addressed by identity - by convention the object's type name combined with its `id`, though the exact key construction is the store's business. Each operation's result is kept as a tree of *references* into that map. Two different documents that both fetched event `ev-4471` point at one stored entity, not at two copies. That single fact is the whole answer. A mutation response is not special. It is parsed, shredded and merged into the same map by the same rules. Writing new field values for `Event:ev-4471` is therefore indistinguishable, from the store's point of view, from a query having fetched them - and every screen whose active operation references `Event:ev-4471` is re-delivered the merged entity. None of this is in the GraphQL specification. GraphQL specifies how a document is executed and what the response envelope looks like; identity-keyed client caching is a widespread client-side **convention**, and the Global Object Identification convention exists partly to give such a cache a durable key to use. ## A worked example A ticketing graph. `Event` is a wide type - 37 fields covering the billing, marketing and box-office concerns that accreted onto it - and one of them is `doorsOpenAt`. ```graphql mutation ShiftDoors($id: ID!, $at: DateTime!) { updateEventDoorsOpenAt(eventId: $id, doorsOpenAt: $at) { event { id doorsOpenAt updatedAt } } } ``` The response comes back: ```json {"data":{"updateEventDoorsOpenAt":{"event":{ "id":"ev-4471","doorsOpenAt":"2026-09-14T18:30:00Z","updatedAt":"2026-09-03T11:02:41Z"}}}} ``` The store finds it already holds `Event:ev-4471` with 37 fields, merges the three that arrived over the three it had, and leaves the other 34 alone. The box-office screen, the venue-schedule screen and the printed-ticket preview all read `doorsOpenAt` off that entity, and all three re-render. No query was re-issued; no round trip was spent. ## The three ways this quietly fails **The payload omits `id`.** Without the identity field, the store cannot match the returned object to the one it holds. Some stores then key it by the path it arrived at - effectively a private copy under the mutation result - and the screens keep reading the old, still-stale entity. Selecting `id` in every mutation payload is the cheapest habit on this list. **The payload omits a field that changed.** Suppose the server also recalculates `salesCloseAt` whenever the doors time moves. The document above never asked for it, so the store keeps the old value and one screen shows an inconsistent pair. The client only learns about changes it explicitly selected; a mutation cannot push a field nobody asked for. **The changed object is not the one the screen reads.** Shifting the doors time may change a derived `Venue.tonightSchedule` summary computed by the server. That value lives on a different entity, so refreshing `Event:ev-4471` does nothing for it. Every field on every *other* entity that the write invalidated is invisible to this mechanism, and is exactly what an explicit refetch exists to repair. ## What it categorically cannot do Returning the changed entity fixes **values on an object the store already has**. It does not change **membership**: creating a ticket writes `Ticket:tk-9930` into the map, but nothing appends a reference to it into the `Event.tickets` list that some screen is rendering, so the new ticket is invisible until that list is repaired or refetched. Deleting is the mirror image - the entity may be dropped while stale references to it linger in lists. That membership gap is the reason mutation invalidation is a topic at all rather than a solved problem. ## Why this shape is worth insisting on Returning the changed entity is the cheapest correct invalidation available: it costs no extra round trip, it is impossible to get out of order with the write it belongs to, and it needs no client-side knowledge of which screens are open. Compare it with the alternative of refetching every affected operation, which costs one or more extra requests, lands after an arbitrary delay, and can race the very write that triggered it. The practical rule is: make the payload return the object you changed, selecting `id` plus every field the write can move, and treat refetching as the escalation for what that cannot express.

  • The mutation returned the changed event, but one screen still shows the old doors-open time. What do you check first?
    Two things, in order. Did the payload select `id`? Without it the store cannot match the returned object to the entity the screen reads, so it lands as an unrelated copy. Then, did the payload select the field that screen renders? A store only merges the fields that arrived; anything unselected keeps its old value. Both faults look identical from the UI and both are fixed in the document, not in the store.
  • Does this still work when the mutated object was never in the cache before?
    The object is written to the store either way, but writing it is not the same as showing it. A screen renders what its own operation's result tree references, and a brand-new entity is referenced by nothing. So a create is not repaired by returning the entity - the list that should contain it has to be patched or refetched. Updating an object already on screen is the case the mechanism actually covers.
  • What happens with a type that has no id field?
    The store has no identity to key on, so it cannot merge the returned object with anything - it typically falls back to storing the value inline under the path where it appeared, which means the mutation result and the query result stay separate copies. The fixes are to expose a stable identifier on the type, or to configure the store with an alternative key for it. Value-object types with no identity are legitimately non-normalizable and simply do not get this behaviour.

The store is a filing cabinet with one folder per object, and every screen holds a pointer to a folder rather than its own photocopy. A mutation that returns the object just files new pages in the folder everyone is already pointing at.

saying these in an interview costs you the question

  • Thinks the client re-runs every query after any mutation
  • Believes the store matches on the mutation field name, not identity
  • Omits id from the payload and still expects a merge
  • Assumes fields the payload did not select are refreshed too
  • Claims returning the entity also fixes lists containing it
  • Calls identity-keyed client caching part of the GraphQL specification

context

open as a page

Why does a normalized client cache store one entry per argument set for a paginated list field?

level: juniorimportance: must knowfreq 56%

basics

~20 s

A cached field is stored under a key made of the field name plus the arguments it was fetched with, because different arguments mean a different answer. Each page, fetched with a different cursor argument, therefore lands in its own entry.

open as a page

What does a normalized GraphQL client cache store, and how does it differ from a document cache?

level: juniorimportance: must knowfreq 68%

basics

~20 s

A normalized client cache shreds each response into individual objects stored under an identity key, usually the object's __typename plus its id, with nested objects replaced by references. A document cache instead stores a whole response under the operation plus its variables.

open as a page

With a build-time persisted document registry, what does a GraphQL request carry instead of the document text?

level: juniorimportance: must knowfreq 51%

basics

~20 s

An identifier for a document the server already holds, plus the variables. A build step extracts every operation from the client source into a manifest and publishes it ahead of the release; the shipped client carries identifiers, never query text.

open as a page

Why can't an HTTP cache reuse a GraphQL response when every operation is POSTed to one URL?

level: juniorimportance: must knowfreq 71%

basics

~20 s

HTTP 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.

open as a page

In Automatic Persisted Queries, what does the client send first and what happens on a miss?

level: middleimportance: must knowfreq 58%

basics

~20 s

The client sends only its document's SHA-256 hash, no query text. A server that knows the hash executes it; a server that does not returns a not-found error, and the client re-sends with the document text, which the server stores and runs.

open as a page

How does a GraphQL server combine per-field cache hints into one response lifetime?

level: middleimportance: must knowfreq 55%

basics

~20 s

The server takes the minimum max age across every field the document selected, and the most restrictive scope: one private field makes the whole response private. A field with no hint contributes zero, which makes the response uncacheable.

open as a page

Why does interpolating a value into a GraphQL document's text instead of sending a variable hurt the server?

level: middleimportance: must knowfreq 62%

basics

~20 s

Every distinct value produces distinct document text, so the server's parse-and-validate cache misses on nearly every request and re-does that work. It also splits per-operation metrics into one series per value, and no document allowlist can ever match.

open as a page

Why can a CDN store a GraphQL response that reports a failure, and how do you stop it?

level: middleimportance: must knowfreq 58%

basics

~20 s

A shared cache decides from the HTTP status line, but GraphQL reports field failures in the body — a partly-null response carrying an errors entry still arrives as 200. The origin must mark those responses uncacheable before they leave.

open as a page

Why does a cacheable GraphQL GET carry an operation identifier rather than the document text?

level: middleimportance: must knowfreq 54%

basics

~20 s

Size and key stability. A percent-encoded document is long enough to approach URL limits, and because the whole URL is the cache key, any difference in formatting or parameter order between clients mints a second entry for the same operation.

open as a page

A GraphQL mutation returns the created object, but a cached list still omits it. Why?

level: middleimportance: must knowfreq 60%

basics

~20 s

Membership belongs to the list field, not to the created object. A normalized store writes the new entity, but nothing appends a reference to it in the stored list, so the screen renders the old membership until the list is patched or refetched.

open as a page

What does a per-field cache hint on a GraphQL schema declare, and what consumes it?

level: juniorimportance: should knowfreq 38%

basics

~20 s

A cache hint declares how many seconds a field's value stays fresh and whether it is public to every viewer or private to one. The server combines the hints of the selected fields into a single caching policy for the response.

open as a page

How should a normalized cache merge an incoming page into an already-cached list field?

level: middleimportance: should knowfreq 46%

basics

~20 s

A merge rule receives what is already stored under the field's key plus the incoming page, and returns the value to store. It must place the incoming items by the request's arguments rather than blindly concatenating them.

open as a page

Why does a normalized client cache miss when a read selects a field it never stored?

level: middleimportance: should knowfreq 51%

basics

~20 s

A read runs the whole selection set against the entity store and must find every selected field. One field absent from an entity makes the read incomplete, so the client reports a miss and goes to the network rather than returning a partly filled object.

open as a page

Why must a persisted-operation manifest reach the server before the client build that uses it ships?

level: middleimportance: should knowfreq 46%

basics

~20 s

Because the identifier means nothing until the server holds the mapping, and the shipped client has no document text to fall back on. Ship the client first and every one of its requests fails until the manifest lands.

open as a page

Which Automatic Persisted Queries requests pay two round trips, and why does that keep recurring?

level: seniorimportance: should knowfreq 46%

basics

~20 s

One request pays two trips per document per document-store — and the store is usually per server process, so the cost repeats on every replica, every deploy, every restart and every scale-out. Misses are a steady rate, not a one-off warm-up.

open as a page

A GraphQL response stopped being shared-cacheable after one per-viewer field was added. Why, and what would you change?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Scope folds to the most restrictive value, so one field hinted private makes the entire response private and no cache shared between users may store it. The fix is to move the per-viewer field out of that document into its own operation.

open as a page

How can a GraphQL server's parsed-document cache exhaust memory, and how do you bound it safely?

level: seniorimportance: should knowfreq 41%

basics

~20 s

Its keys come from caller-supplied document text, so anyone sending unique text grows it without limit until the process runs out of heap. Bound it with a capped evicting cache, a size check before parsing, and hashed keys.

open as a page

What must an edge cache key contain for a GraphQL query sent over GET?

level: seniorimportance: should knowfreq 47%

basics

~20 s

An identity for the operation that will run, the variables in a canonical form, and — whenever a selected field depends on who is asking — a dimension of the caller's identity. Miss the third and one viewer receives another's data.

open as a page

Some GraphQL queries sent over GET fail with an HTTP error and no response body — how do you diagnose it?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Suspect URL length. A percent-encoded document plus its variables can exceed the request-line or header limit of a browser, proxy or server, which rejects the request — often 414 URI Too Long — before any GraphQL execution.

open as a page

A retried GraphQL mutation leaves a duplicate row in the client cache after an optimistic update. What went wrong?

level: seniorimportance: should knowfreq 44%

basics

~20 s

The first attempt reached the server and committed; only its response was lost. The retry created a second object with a second id, so the two real responses wrote two entities and two list references. Rollback cannot help - nothing failed.

open as a page

In a normalized cache, which arguments of a paginated list field belong in the storage key?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Arguments that decide which set of items the field denotes stay in the key: filters, sort order, scope. Arguments that only choose a window into that set are dropped, so every page accumulates into one entry.

open as a page

If POSTed GraphQL responses are never stored by HTTP caches, why can a field still return stale data?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Losing the HTTP cache tier relocates caching rather than removing it. Client stores, server-side response caches, batch loaders and the data layer behind resolvers all still cache — below the boundary, where no header, metric or URL purge reaches them.

open as a page

How do you decide which GraphQL operations belong behind an edge cache and which stay at the origin?

level: principalimportance: should knowfreq 44%

basics

~20 s

Per operation, never as a global switch. Promote reads whose origin cost times request rate is large, whose variables have low cardinality, that carry no viewer-scoped field, and whose staleness budget you can state in seconds.

open as a page

How do you set a team-wide policy for keeping a GraphQL client cache correct after mutations?

level: principalimportance: should knowfreq 38%

basics

~20 s

Pick a default that is correct without thought - mutations return the objects they changed, and membership changes refetch the affected operations - then allow hand-written store patches only on measured hot paths, and measure the refetch traffic the policy costs.

open as a page

Is the persistedQuery extension defined by the GraphQL specification, or is it a convention?

level: juniorimportance: nice to knowfreq 24%

basics

~20 s

A convention, not specification. GraphQL reserves an extensions map for implementation-defined data and defines nothing named persistedQuery; the entry carrying a version number and a SHA-256 hash is a shape clients and servers agree on privately.

open as a page

What does a GraphQL server store in a parsed-document cache, and what is the cache key?

level: juniorimportance: nice to knowfreq 30%

basics

~20 s

It stores the parsed syntax tree for a document plus the verdict that it validated cleanly against the current schema, keyed by the exact document text or a hash of it. Variables are never part of the key.

open as a page

What does a GraphQL server return when a GET request's document contains a mutation?

level: juniorimportance: nice to knowfreq 26%

basics

~20 s

An HTTP-level refusal, not a GraphQL result. The GraphQL over HTTP working draft has the server answer 405 Method Not Allowed and execute nothing, so there is no data/errors envelope for the client to parse.

open as a page

Under HTTP, when may a cache store a response to a POST, and why doesn't that help a GraphQL endpoint?

level: middleimportance: nice to knowfreq 23%

basics

~20 s

HTTP 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.

open as a page

Why is a GraphQL request's operationName unsafe as a shared cache key?

level: seniorimportance: nice to knowfreq 19%

basics

~20 s

Because operationName only picks which operation to run out of a document that defines several. It is a client-chosen label, not an identity: two unrelated documents may reuse it, and it says nothing about the variables.

open as a page

showing 1–30 of 32