# Cache GraphQL responses with Zuplo

GraphQL clients send queries as POST request bodies, so a normal CDN — which
keys on the URL — can't cache them. The
[`graphql-cache-inbound`](../policies/graphql-cache-inbound.mdx) policy solves
this: it parses each query, normalizes it into a canonical form, and caches the
response in a [ZoneCache](../programmable-api/zone-cache.mdx) at the edge. Two
requests that are semantically identical share a cache entry even when their
bodies differ byte-for-byte.

Only `query` operations are cached. Mutations, subscriptions, malformed
documents, and responses that contain GraphQL `errors` are always forwarded to
your origin untouched.

## Add the cache policy

Add `graphql-cache-inbound` to the inbound policies of your GraphQL route.

<Stepper>

1. **Open `policies.json`**

   In the Zuplo Portal, open the **Code** tab and add the policy definition:

   ```json title="config/policies.json"
   {
     "name": "graphql-cache",
     "policyType": "graphql-cache-inbound",
     "handler": {
       "export": "GraphQLCacheInboundPolicy",
       "module": "$import(@zuplo/graphql)",
       "options": {
         "cacheName": "graphql-responses",
         "ttlSeconds": 60
       }
     }
   }
   ```

2. **Attach it to your GraphQL route**

   Add `"graphql-cache"` to the route's inbound policies in `routes.oas.json`,
   ahead of any policy that rewrites the request.

3. **Verify the cache is working**

   Send the same query twice and inspect the response headers. The policy adds
   `x-cache: MISS` on the first request and `x-cache: HIT` on the second. The
   `x-cache-key` header (first 8 characters of the cache key) confirms two
   requests resolve to the same entry.

</Stepper>

## Cache authenticated traffic safely

By default, requests that carry an `authorization` or `cookie` header are
**not** cached — otherwise the first user's response would be served to
everyone. To cache per-user, list the headers that make a response user-specific
in `cacheKeyHeaders`. Each distinct value gets its own entry:

```json title="config/policies.json"
{
  "options": {
    "ttlSeconds": 30,
    "cacheKeyHeaders": ["authorization"]
  }
}
```

:::caution

Setting `cacheKeyHeaders` to an empty array `[]` caches a single response and
shares it across all callers. Only do this when the response genuinely doesn't
depend on who is calling — otherwise one caller's data leaks to others.

:::

See the [GraphQL Cache policy reference](../policies/graphql-cache-inbound.mdx)
for the full option matrix, including how credentialed requests fail safe.

## Next steps

- [Secure your GraphQL API](./graphql-security.mdx) — complexity limits and
  introspection controls
- [GraphQL on Zuplo](./graphql.mdx) — set up the endpoint and Dev Portal docs
- [GraphQL analytics](../analytics/tabs/graphql.md) — watch hit rates and
  latency once traffic flows
