Lifetime from the origin header
Your origin’s Cache-Control response header is how any origin, plain HTTP or GraphQL, tells
GraphPilot how long a response may be kept. For a plain HTTP response it is the only source: no
header, or a header without a lifetime, means the response is not cached. For a GraphQL response
it outranks your schema whenever it says anything about storage or freshness; see
precedence.
Cache-Control: max-age=30, s-maxage=600, stale-while-revalidate=60The directives GraphPilot reads
Section titled “The directives GraphPilot reads”| Directive | What GraphPilot does |
|---|---|
max-age=<s> | The lifetime, unless s-maxage is also present. |
s-maxage=<s> | The lifetime at the edge. Overrides max-age, which is left to the browser. Also permission to share a response to a signed-in caller. |
public | Permission to share a response to a signed-in caller. States no lifetime on its own. |
private | The entry is partitioned per caller under your cache. expression. Not stored if that does not resolve for the request (CACHE_). |
no-store | Not stored (CACHE_). |
no-cache | Not stored (CACHE_). See below. |
stale- | A stale window after the lifetime. |
must-, proxy- | The stale window is dropped: the entry is stored for its lifetime only (CACHE_ in your logs). Not accepted as permission to share. |
stale- | Parsed, and not acted on. GraphPilot does not serve a stale entry when your origin fails. |
immutable, must- | Parsed. They change nothing beyond making the header one that decides. |
no-transform | Parsed. States no lifetime, so a header made only of it yields to the schema. |
Directives are matched case-insensitively, a header sent several times is read as one list, and a directive GraphPilot does not know is ignored, as RFC 9111 asks.
When the header decides, it is checked in this order, and the first rule that refuses ends it:
no-storerefuses.no-cacherefuses.- The lifetime comes from
s-maxage, otherwisemax-age. With neither, the response is refused asCACHE_SKIPPED_NO_MAX_AGE. This comes before the partitioning check, so aprivateresponse with nomax-agereportsCACHE_SKIPPED_NO_MAX_AGE. - Every discriminator the entry is keyed on has to be available: your
cache.policy.privatevalue for aprivateresponse, and the names of any scope your schema set for the same query. See who shares an entry. - A response to a request that carried a credential is shared only when the header says
publicors-maxage, or names the credential inVary. Otherwise it is partitioned or not stored. See authenticated requests.
The CACHE_POLICY_ORIGIN_HEADER log line records that the header
decided, carries it verbatim as cache_control, and reports overrode_schema_policy = true when
there was a schema policy it outranked.
s-maxage: a different lifetime at the edge than in the browser
Section titled “s-maxage: a different lifetime at the edge than in the browser”max-age is addressed to every cache, the browser’s included. s-maxage is addressed to
shared caches only, and GraphPilot is one. Where both are present, s-maxage decides how
long the edge keeps the response and max-age is left to the browser, per
RFC 9111.
The right answer is rarely the same number for both:
Cache-Control: max-age=30, s-maxage=600Ten minutes at the edge, thirty seconds in the browser. The edge is one cache serving everybody, so a long lifetime there absorbs a lot of origin traffic, and you can purge it the moment the data changes. A browser cache is one per person, you cannot reach it, and nothing you do invalidates it before it expires.
Reversed, it says “cache this for the user, not for everyone”:
Cache-Control: max-age=600, s-maxage=0s-maxage=0 keeps a response from ever being served fresh from the edge. If you mean that the
edge should keep nothing at all, send no-store: s-maxage=0 next to a
stale-while-revalidate is still served stale.
no-cache is refused, not revalidated
Section titled “no-cache is refused, not revalidated”no-cache permits storing a response but forbids serving it without revalidating with the origin
first, on every use. GraphPilot never sends a conditional request and never handles a 304, so
an entry it stored could only be served in breach of that instruction. It does not store one.
Use max-age if you want the response cached, and no-store if you want it neither stored nor
served from a cache.
Headers GraphPilot does not read
Section titled “Headers GraphPilot does not read”GraphPilot reads exactly one response header to decide a lifetime: Cache-Control.
Surrogate-Controlis ignored. If you come from a CDN that reads it, move itsmax-ageintoCache-Controlass-maxage, which says the same thing to every shared cache.Expiresis ignored. State the lifetime withmax-ageors-maxage.Age,Date,ETagandLast-Modifiedare not consulted either. An entry’s age comes from the cache itself.
Which responses are refused whatever their Cache-Control says (Set-Cookie, Vary: * and
others) is on when a response is cached.
Apollo Server sends its own header
Section titled “Apollo Server sends its own header”Apollo Server’s cache-control plugin computes a Cache-Control header from the same
@cacheControl annotations GraphPilot reads, and sends it on every response:
max-age=N, public, max-age=N, private, or no-store for an operation it considers
uncacheable. Because
an origin header wins, GraphPilot then reads your schema hints and never applies them: the header
your server already sends is authoritative.
That changes nothing when the header is what you want. If you would rather GraphPilot decide from the schema, stop Apollo Server from sending the header; see Apollo Server.
Related
Section titled “Related”- How long an entry lives: precedence between header and schema
- Stale windows
- Authenticated requests: when
publicors-maxageis required - Downstream header: your header reaches the client unchanged