GraphQL Cache Policy
This policy caches GraphQL query responses at the edge so identical queries are served without a round-trip to your origin.
Unlike CDN caching that keys on the raw request body, this policy parses each GraphQL document and normalizes it before building a cache key. Insignificant whitespace, field formatting, and fragment layout are collapsed, and variable object keys are sorted. As a result, two requests that are semantically identical share a cache entry even when their bodies differ byte-for-byte — and there is no query size or nesting-depth limit.
Only query operations are cached. Mutations, subscriptions, malformed
documents, and non-GraphQL bodies are always forwarded to the origin untouched.
To avoid serving one user's data to another, requests carrying an
authorization or cookie header are not cached unless you opt in with the
cacheKeyHeaders option.
Configuration
The configuration shows how to configure the policy in the 'policies.json' document.
config/policies.json
Policy Configuration
name<string>- The name of your policy instance. This is used as a reference in your routes.policyType<string>- The identifier of the policy. This is used by the Zuplo UI. Value should begraphql-cache-inbound.handler.export<string>- The name of the exported type. Value should beGraphQLCacheInboundPolicy.handler.module<string>- The module containing the policy. Value should be$import(@zuplo/graphql).handler.options<object>- The options for this policy. See Policy Options below.
Policy Options
The options for this policy are specified below. All properties are optional unless specifically marked as required.
cacheName<string>- The name of the cache used to store responses. Routes that share a name share a cache; use distinct names to isolate caches per route or per upstream. Defaults to"graphql-responses".ttlSeconds<number>- How long, in seconds, a cached response is served before it is considered stale and the next request is forwarded to the origin to refresh the entry. Defaults to60.cacheKeyHeaders<string[]>- Request header names whose values are included in the cache key (matched case-insensitively), and the control for how credentialed requests are cached. Omit this option (the default) and requests carrying anauthorizationorcookieheader are not cached, to avoid serving one user's response to another. List those headers to cache such requests keyed per value, so each distinct value gets its own entry (a credential header you don't list still blocks caching, so a partial list fails safe). Set it to an empty array[]to cache a single response shared across all callers — only do this when the response does not depend on who is calling, as it disables the per-user safety check.
Using the Policy
The GraphQL Cache policy stores successful GraphQL query responses in a ZoneCache and serves later, identical queries directly from the edge.
How caching works
For every inbound request the policy:
- Reads the request body and parses it as GraphQL JSON
(
{ "query": "...", "variables": { ... }, "operationName": "..." }). - Parses the query and re-prints it, producing a canonical form that ignores insignificant whitespace, field formatting, and fragment layout.
- Canonicalizes the
variablesby recursively sorting object keys. - Hashes the normalized query, canonicalized variables, and
operationName(SHA-256) into a cache key.operationNameis included because a document with multiple operations returns a different response depending on which operation the client selects.
On a hit, the cached response is returned immediately. On a miss, the request is forwarded to the origin and a successful response is stored for future requests.
Every response served or stored by the policy carries two headers:
x-cache—HITwhen served from cache,MISSwhen fetched from the origin.x-cache-key— the first 8 characters of the cache key, useful for confirming that two requests resolve to the same entry.
Both headers are added to access-control-expose-headers so browsers can read
them, without overwriting any value an upstream CORS policy already set.
What is and isn't cached
- Only
queryoperations are cached. Mutations and subscriptions are forwarded to the origin and never cached. In a multi-operation document the operation selected byoperationNameis the one that decides this. - Malformed GraphQL and non-JSON bodies are forwarded untouched so the
origin can return a proper error. Documents with multiple operations but no
operationName(or anoperationNamethat matches none) are also forwarded. - Only
200responses are cached, and only when the body is a successful GraphQL result. Because GraphQL returns execution errors with a200status and anerrorsarray, responses carrying a non-emptyerrorsarray — and non-JSON200bodies — are not cached.
Options
cacheName- The name of the cache used to store responses. Defaults tographql-responses. Routes that share a name share a cache; use distinct names to isolate caches per route or per upstream.ttlSeconds- How long, in seconds, a cached response is served before it is considered stale and the next request is forwarded to the origin to refresh the entry. Defaults to60.cacheKeyHeaders- Request header names whose values are included in the cache key (matched case-insensitively), and the control for how credentialed requests are cached. See Authentication and per-user caching below. Defaults to omitted.
Authentication and per-user caching
A response cache keyed only on the query would serve the first user's response
to everyone. To prevent that, the policy does not cache requests that carry an
authorization or cookie header by default — those requests are forwarded
to the origin every time.
To cache authenticated traffic safely, list the headers that make a response
user-specific in cacheKeyHeaders. Each listed header's value becomes part of
the cache key, so every distinct value (for example, every bearer token) gets
its own cache entry:
Code
If a request still carries an authorization or cookie header that is not
in cacheKeyHeaders, it is left uncached — so partially configuring the
allowlist fails safe rather than leaking across users.
Caching credentialed requests as a single shared entry
Sometimes a request must carry authorization (or cookie) to be authorized,
but the response is identical for everyone allowed through — the credential
gates access without changing the data. In that case, set cacheKeyHeaders to
an empty array to cache one response and share it across all callers:
Code
This is distinct from omitting the option: omitting it keeps the safe default
(credentialed requests are not cached), whereas [] is an explicit assertion
that the response does not depend on the caller. Only use [] when that is
true — otherwise one caller's response will be served to others.
Response cookies are never shared. Set-Cookie (along with Set-Cookie2 and
Clear-Site-Data) is stripped from the stored entry, so a cookie an origin sets
on one caller's response is never replayed to another from cache. The caller
whose request reached the origin still receives the original Set-Cookie.
Example
Code
Read more about how policies work