ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop on the web portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
    Develop locally with the CLI
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
Concepts
Development
Policies
    Policy Catalog
    Authentication
    Authorization
    MCP Authorization
    Security & Validation
    Metrics, Billing & Quotas
    Testing
    Request Modification
    Response Modification
    Upstream Authentication
    Archival
    GraphQL
      GraphQL CacheGraphQL Complexity LimitGraphQL Disable IntrospectionGraphQL Introspection FilterGraphQL Analytics
    Other
    Guides
Handlers
API Keys
Rate Limiting
MCP Server
MCP Gateway
AI Gateway
Developer Portal
Monetization
GraphQL
Deploying & Source Control
Analytics
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
GraphQL

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
{ "name": "my-graphql-cache-inbound-policy", "policyType": "graphql-cache-inbound", "handler": { "export": "GraphQLCacheInboundPolicy", "module": "$import(@zuplo/graphql)", "options": { "cacheKeyHeaders": ["authorization"], "cacheName": "graphql-responses", "ttlSeconds": 60 } } }

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 be graphql-cache-inbound.
  • handler.export <string> - The name of the exported type. Value should be GraphQLCacheInboundPolicy.
  • 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 to 60.
  • 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 an authorization or cookie header 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:

  1. Reads the request body and parses it as GraphQL JSON ({ "query": "...", "variables": { ... }, "operationName": "..." }).
  2. Parses the query and re-prints it, producing a canonical form that ignores insignificant whitespace, field formatting, and fragment layout.
  3. Canonicalizes the variables by recursively sorting object keys.
  4. Hashes the normalized query, canonicalized variables, and operationName (SHA-256) into a cache key. operationName is 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 — HIT when served from cache, MISS when 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 query operations are cached. Mutations and subscriptions are forwarded to the origin and never cached. In a multi-operation document the operation selected by operationName is 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 an operationName that matches none) are also forwarded.
  • Only 200 responses are cached, and only when the body is a successful GraphQL result. Because GraphQL returns execution errors with a 200 status and an errors array, responses carrying a non-empty errors array — and non-JSON 200 bodies — are not cached.

Options

  • cacheName - The name of the cache used to store responses. Defaults to graphql-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 to 60.
  • 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
{ "name": "graphql-cache", "policyType": "graphql-cache-inbound", "handler": { "export": "GraphQLCacheInboundPolicy", "module": "$import(@zuplo/graphql)", "options": { "ttlSeconds": 30, "cacheKeyHeaders": ["authorization"] } } }

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
{ "name": "graphql-cache", "policyType": "graphql-cache-inbound", "handler": { "export": "GraphQLCacheInboundPolicy", "module": "$import(@zuplo/graphql)", "options": { "cacheKeyHeaders": [] } } }

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
{ "name": "graphql-cache", "policyType": "graphql-cache-inbound", "handler": { "export": "GraphQLCacheInboundPolicy", "module": "$import(@zuplo/graphql)", "options": { "cacheName": "graphql-responses", "ttlSeconds": 60 } } }

Read more about how policies work

Edit this page
Last modified on June 29, 2026
Archive Response to AWS S3GraphQL Complexity Limit
On this page
  • Configuration
    • Policy Configuration
    • Policy Options
  • Using the Policy
    • How caching works
    • What is and isn't cached
    • Options
    • Authentication and per-user caching
    • Example
JSON
JSON
JSON
JSON