skip to content

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

level: juniorimportance: should knowfreq 38%

answer

  1. Freshness is a property of the data
  2. Declared in the schema, not the document
  3. Two parts: a duration and an audience
  4. Seconds of max age, plus public or private
  5. Server folds selected fields into one policy

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.

solid answer

~50 s

A cache hint is attached in the **schema**, next to the field or type definition, conventionally as a type-system directive (commonly spelled `@cacheControl`). It carries a **max age** in seconds and a **scope** that is either public - the same value for every caller - or private, meaning one viewer's value. Nothing about this is in the GraphQL specification; the specification says nothing about caching at all, so spellings and defaults vary by server while the idea is near-universal. What consumes the hints is the **server**, at the end of executing one operation: it walks the fields the document actually selected, folds their hints into one policy, and attaches that policy to the HTTP response for downstream caches to read. Because it is computed over the selected fields, two documents against the same schema get different lifetimes.

code

graphql · 6 lines
graphql
type Claim @cacheControl(maxAge: 3600) {
  id: ID!
  policyType: PolicyType!
  openTaskCount: Int! @cacheControl(maxAge: 30)
  adjusterNotes: String @cacheControl(maxAge: 0, scope: PRIVATE)
}

go deeper

for a junior

Be ready to say the two parts of a hint out loud - a lifetime in seconds and a public-or-private scope - and to state that it is written in the schema next to the field, not sent by the client.

for a middle

Explain the mechanics: hints on fields and on type definitions, inheritance between them, and the fact that the server computes one policy per executed operation rather than one per schema.

for a senior

Show that you treat a hint as a declared business freshness contract. Talk about who decides the number, how a runtime override narrows it for a particular object, and why private scope is a caching statement rather than an authorization one.

for a principal

Own the policy question: whether freshness belongs in the schema at all, how you keep hints honest as a schema grows across teams, and what you would rather pay for - a shorter lifetime everywhere or a per-consumer conversation about staleness.

## The problem a hint solves An HTTP cache decides whether to store a response, and for how long, by reading freshness information the origin puts on that response. That model assumes the origin knows one answer per response, which is easy when a response is one resource. A GraphQL response is not one resource. It is an assembly of fields, and those fields have wildly different freshness. In an insurance claims graph, a claim's `policyType` changes once in the life of the policy, its `openTaskCount` changes several times an hour, and a `viewerCanReopen` flag is different for every person who asks. Yet there is exactly one response body, and it needs exactly one answer to "how long is this good for, and who may be shown it?" A cache hint is how the schema supplies the raw material for that answer. ## What a hint declares A hint lives where the field is **declared** - in the schema - not in the executable document a client sends. It is conventionally a type-system directive, most often spelled `@cacheControl`, and it carries two things: - **A max age**: a number of **seconds** this field's value may be treated as fresh. Seconds, not milliseconds. - **A scope**: `PUBLIC`, meaning the value is identical for every caller and may live in a cache shared between users, or `PRIVATE`, meaning the value belongs to one viewer and must not. Both are statements about the **data**, not about one request. "An adjuster's notes are per-viewer" and "a settled claim's amount is stable for an hour" are properties of the domain, which is precisely why they belong in the schema rather than in each caller's code. ## Where a hint can be attached Two placements are usual. On a **field definition**, the hint governs that field. On an **object type definition**, it supplies the default for fields whose return type is that object - so `type Claim @cacheControl(maxAge: 3600)` says "anything that hands back a Claim is good for an hour unless the field itself says otherwise." A field's own hint wins over the one it inherits. Many servers also let a resolver narrow the hint **at execution time**, because freshness sometimes depends on the value: a settled claim is stable for hours, the same field on a claim still in review is not. The declared hint is the default; the runtime one refines it for that particular object. ## What consumes it The server does, once per operation. After execution it walks the fields the document actually selected, folds their hints together into one policy - one lifetime and one scope - and attaches that policy to the HTTP response, where the ordinary HTTP caching machinery takes over. From there a browser's own cache, a shared cache in front of the service, or a response cache inside the server reads it like any other response's freshness information. Two consequences follow from "per operation". First, the same schema yields **different lifetimes for different documents**: a document selecting only stable fields gets a long one, a document that adds one volatile field gets a short one. Second, a hint on a field nobody selected is irrelevant - it never enters the computation. A separate precondition is worth knowing: a cache outside the process only ever sees a GraphQL response if the operation travelled in a form that layer is willing to cache in the first place. Hints decide *how long*; they do not by themselves make a response visible to a shared cache. ## What it is not **Not the specification.** The GraphQL specification defines the type system, validation, execution and the response format. It has nothing to say about caching, TTLs, or a directive named for it. Cache hints are a widespread server convention. Treat argument names, defaults and even the directive's spelling as things that differ between servers, and never claim in an interview that "the spec defines max age". **Not authorization.** A `PRIVATE` scope does not stop anyone from selecting or reading the field. It stops a cache shared between users from storing the response. Who may read `adjusterNotes` is a field-authorization question with a completely different mechanism, and conflating the two is the classic junior error. **Not per-field caching.** The hint's granularity is declaration-side only. What ends up stored is the whole response body under one lifetime - there is no HTTP cache that keeps `policyType` for an hour and `openTaskCount` for thirty seconds out of the same body. Per-field lifetimes exist only in a client's normalized store or a server-side field cache, which are different machinery. **Not a promise about mutations.** Responses to mutations are conventionally not cached at all, so a hint on a mutation field is at best inert and at worst misleading.

  • Besides a single field, where else can a cache hint be attached, and what does that placement mean?
    On an object type definition. There it supplies the default lifetime and scope for every field whose return type is that object, so one line covers a whole family of fields. A hint written on the field itself overrides the inherited one. Attaching a hint to a type is how teams avoid annotating hundreds of fields individually while still keeping the volatile handful explicit.
  • Does a cache hint on a mutation field do anything useful?
    Effectively no. By convention a mutation response is not cached - it reflects a state change made by one caller, and re-serving it to anyone else is meaningless or harmful - so servers do not emit a positive lifetime for mutation responses. A hint sitting on a mutation field is inert decoration, and a reviewer should read it as a sign the author has not distinguished cacheability from freshness.
  • If a field's max age is a property of the data, why do teams still argue about the number?
    Because the number encodes a business tolerance for staleness, not a technical fact. "How wrong may a claim's open-task count be on a dashboard?" is a product question, and different consumers of the same field answer it differently. The schema forces a single answer per field, which is why the conversation usually ends up between the API owners and the people who read the screen.

The schema labels each field the way a shop labels stock: a use-by date and whether the item is on the open shelf or reserved for one named customer.

saying these in an interview costs you the question

  • Claims cache hints are defined by the GraphQL specification
  • Says max age is expressed in milliseconds
  • Thinks a private scope blocks unauthorized users from reading the field
  • Believes each field is cached separately with its own lifetime
  • Puts the hint in the client's document instead of the schema
  • Assumes an unhinted field is cached indefinitely

context