Apollo Router
OverviewGet StartedRequest Lifecycle
Configuration
Features

Security

Observability

Performance and Scaling

Client Features

Query Planning

Customization

Deployment
Releases
GraphOS Integration
Reference

Response Cache Customization

Cache private data and customize cache keys

Requires ≥ Router v2.10.0

Beyond basic response caching, the router supports customization for caching private data, modifying cache keys, and advanced Redis configuration.

When to customize

Consider these advanced customization options when:

  • You need to cache user-specific data (PRIVATE cache scope)

  • Cache entries should vary based on request headers (locale, tenant ID, feature flags)

  • You require multiple Redis instances for different subgraphs

  • You need fine-tuned Redis performance (connection pools, timeouts, namespacing)

For basic response caching setup, see the Quickstart page.

note
Where caching sits in the request pipeline determines what still runs on a cache hit:
  • Subgraph responses are cached at the subgraph service level, right after coprocessor and Rhai plugins, so the coprocessor and Rhai subgraph stages still run on a cache hit (both at request and response level).
  • Connector root fields are cached at the connector HTTP request level, so the coprocessor's connector_request/connector_response stages still run on a cache hit. A cached response is replayed as an ordinary connector response, carrying the status the API returned when the entry was stored and its cache-control headers, so your coprocessor does not need to distinguish a cache hit from a live call. Response headers other than cache-control are not stored, so they are absent on a hit.
  • Connector entity fetches are cached above make_requests, where one GraphQL fetch fans out into several HTTP requests. A fully cached entity fetch therefore performs no connector requests at all, and runs none of the layers attached to them: no coprocessor stages, and no connector-level telemetry, header propagation, traffic shaping or request limits. When only some entities are cached, those layers run for the representations that missed.
Rhai has no connector hook at all: connector requests and responses cannot be customized with Rhai, cached or not.

Private data caching

A subgraph can return a response with the header Cache-Control: private, indicating that it contains user-personalized data. Although this usually forbids intermediate servers from storing data, the router can recognize different users and store their data in different parts of the cache.

Use private data caching when:

  • Your subgraph returns user-specific data that can be cached (shopping cart, user preferences, personalized recommendations)

  • You can reliably identify users through authentication tokens or session data

  • The performance gain from caching user-specific data outweighs the complexity of managing separate cache entries per user

Don't use private data caching if:

  • Your data contains highly sensitive information that should never be cached

  • You can't reliably identify users across requests

  • User-specific data changes too frequently to benefit from caching

Configure private_id

To set up private information caching, configure the private_id option. This option is a string pointing at a field in the request context that contains data used to recognize users (for example, a user ID, or sub claim in JWT).

As an example, if you are using the router's JWT authentication plugin, first configure the private_id option in the accounts subgraph to point to the user_id key in the context. Then, use a Rhai script to set that key from the JWT's sub claim:

YAML
router.yaml
1response_cache:
2  enabled: true
3  subgraph:
4    all:
5      enabled: true
6      redis:
7        urls: ["redis://..."]
8    subgraphs:
9      accounts:
10        private_id: "user_id"
11authentication:
12  router:
13    jwt:
14      jwks:
15        - url: https://auth-server/jwks.json
Rhai
main.rhai
1fn supergraph_service(service) {
2  let request_callback = |request| {
3    let claims = request.context[Router.APOLLO_AUTHENTICATION_JWT_CLAIMS];
4
5    if claims != () {
6      let private_id = claims["sub"];
7      request.context["user_id"] = private_id;
8    }
9  };
10
11  service.map_request(request_callback);
12}

For connector sources, configure private_id in the connector block:

YAML
router.yaml
1response_cache:
2  enabled: true
3  connector:
4    all:
5      enabled: true
6      cache_key_headers: [] # Required: request headers to include in the cache key ([] = none)
7      redis:
8        urls: ["redis://..."]
9    sources:
10      "accounts.rest_api":
11        private_id: "user_id"

How private data caching works

The router performs the following sequence to determine whether a particular query returns private data:

  1. Upon seeing a query for the first time, the router requests the cache as if it were a public-only query.

  2. When the subgraph returns the response with private data, the router recognizes it and stores the data in a user-specific part of the cache.

  3. The router stores the query in a list of known queries with private data.

  4. When the router subsequently sees a known query:

  • If the private ID isn't provided, the router doesn't check the cache and instead transmits the subgraph response directly.

  • If the private ID is provided, the router queries the part of the cache for the current user and checks the subgraph if nothing is available.

Custom cache keys

To store data for a particular request in different cache entries, configure the cache key through the apollo::response_cache::key context entry.

Use custom cache keys when you need to:

  • Cache different versions of the same query based on request headers (locale, currency, feature flags)

  • Segment cache entries by tenant, region, or API version

  • Include request-specific context that affects the response but isn't part of the GraphQL query

Configure cache keys

You can customize the response cache key with the apollo::response_cache::key context entry. Data in this entry modifies the data used to generate the cache key, and it can be any valid JSON.

You can apply customizations at multiple scales using different fields in apollo::response_cache::key:

  • An all field, which affects all subgraph and connector requests

  • A subgraphs field, which contains an object with per-subgraph customization

  • A connectors field, which contains an object with per-connector-source customization, keyed by subgraph_name.source_name

  • A field for each operation name, which affects only a specific operation

Data within these customizations are not merged. The router chooses data in order of precedence: operation name, subgraph, then all. To set common data, you must also add it to the more specific sections.

Example:

JSON
1{
2  "all": 1,
3  "subgraph_operation1": "key1",
4  "subgraph_operation2": {
5    "data": "key2"
6  },
7  "subgraphs": {
8    "my_subgraph": {
9      "locale": "be"
10    }
11  },
12  "connectors": {
13    "my_subgraph.my_source": {
14      "locale": "be"
15    }
16  }
17}

Example in Rhai:

Rhai
main.rhai
1fn supergraph_service(service) {
2  let request_callback = |request| {
3    // Include the request header value of "x-my-new-header" in the primary cache key hash to create a unique cache entry for every value of this header
4    request.context[Router.APOLLO_RESPONSE_CACHE_KEY]["all"] = request.headers["x-my-new-header"]; // Applied on all subgraphs
5  };
6
7  service.map_request(request_callback);
8}

Connector cache-key headers

For Apollo Connectors, the set of client request headers that are folded into the cache key is an explicit, required configuration: cache_key_headers. Because a connector calls your REST API directly, the router cannot know which request headers cause your API to return a different response—so you must declare them. Set it on connector.all: the router refuses to start if connector caching is enabled and all.cache_key_headers is unset, so you cannot enable connector caching without first deciding which headers matter. A source listed under connector.sources inherits all's value, or overrides it (including with an explicit empty list) by setting its own.

YAML
router.yaml
1response_cache:
2  enabled: true
3  connector:
4    all:
5      enabled: true
6      ttl: 5m
7      redis:
8        urls: ["redis://localhost:6379"]
9      # REQUIRED: the client request headers whose values distinguish cache entries.
10      cache_key_headers:
11        - x-user-id
12        - x-tenant
13    sources:
14      products.catalog_api:
15        # Per-source override; replaces (does not merge with) the `all` list for this source.
16        cache_key_headers:
17          - x-region

How it affects the cache:

  • Each listed header's values (matched case-insensitively; all values of a repeated header, in sorted order) become part of the cache key, so two requests that differ only in a listed header get separate cache entries.

  • A header that is not listed does not affect the cache key—two requests that differ only in an unlisted header share one cache entry.

  • An empty list (cache_key_headers: []) is a valid, explicit choice meaning "no request header distinguishes cache entries."

caution
Choosing this list is a safety decision, and both directions have consequences:
  • Omitting a header your API varies on is a data-disclosure risk. If your REST API returns different data based on a header (for example, a per-user identity header) and you do not list it, one caller's cached response is served to another caller who sent a different value. List every request header that changes what your API returns—including any header you forward to the API via @connect's http.headers.
  • Listing a header that doesn't affect the response wastes cache. Each distinct value of a listed header creates a separate entry, so including a high-cardinality header that has no effect on the response (for example, a per-request trace or correlation ID) fragments the cache and drives the hit rate toward zero.
The goal is to list exactly the headers whose values change your API's response—no more, no fewer.

Example: Multi-tenant caching

Cache different responses for each tenant based on a request header:

Rhai
main.rhai
1fn supergraph_service(service) {
2  let request_callback = |request| {
3    // Create separate cache entries for each tenant
4    let tenant_id = request.headers["x-tenant-id"];
5    if tenant_id != () {
6      request.context[Router.APOLLO_RESPONSE_CACHE_KEY]["all"] = tenant_id;
7    }
8  };
9
10  service.map_request(request_callback);
11}

Example: Locale-specific caching

Cache different responses for each locale:

Rhai
main.rhai
1fn supergraph_service(service) {
2  let request_callback = |request| {
3    // Create separate cache entries for each locale
4    let locale = request.headers["accept-language"];
5    if locale != () {
6      request.context[Router.APOLLO_RESPONSE_CACHE_KEY]["all"] = locale;
7    }
8  };
9
10  service.map_request(request_callback);
11}

Advanced Redis configuration

The router provides multiple Redis options to ensure you can scale properly and achieve the best performance.

For basic Redis setup, see the Quickstart page.

Per-subgraph Redis instances

Configure a global Redis instance (used by default) and override it with specific instances and their own configuration for individual subgraphs:

YAML
router.yaml
1# Enable response caching globally
2response_cache:
3  enabled: true
4  subgraph:
5    all:
6      enabled: true
7      # Configure Redis globally
8      redis:
9        urls: ["redis://..."]
10        fetch_timeout: 300ms # Optional, by default: 150ms
11        insert_timeout: 750ms # Optional, by default: 500ms
12        invalidate_timeout: 750ms # Optional, by default: 1s
13    # Configure response caching per subgraph, overrides options from the "all" section
14    subgraphs:
15      products:
16        redis:
17          urls: ["redis://..."] # Override global Redis instance for this specific subgraph
18          pool_size: 15 # Optional. Default: 5
19          namespace: products_response_cache # Optional. Prefix all the cached entries in Redis with this prefix in Redis key
20      inventory:
21        enabled: false # Disable for a specific subgraph

Per-source Redis instances (connectors)

Configure a global Redis instance for connectors and override it for specific connector sources:

YAML
router.yaml
1response_cache:
2  enabled: true
3  connector:
4    all:
5      enabled: true
6      cache_key_headers: [] # Required: request headers to include in the cache key ([] = none)
7      redis:
8        urls: ["redis://..."]
9    sources:
10      "products.rest_api":
11        redis:
12          urls: ["redis://products-cache:6379"]
13          pool_size: 15
14          namespace: products_connector_cache
15      "inventory.stock_api":
16        enabled: false # Disable caching for this source

Redis URL formats

tip
For comprehensive Redis URL configuration details, including authentication and TLS setup, see the distributed caching Redis configuration guide.

The response caching configuration must contain one or more URLs using different schemes depending on the expected deployment:

  • redis - TCP connected to a centralized server

  • rediss - TLS connected to a centralized server

  • redis-cluster - TCP connected to a cluster

  • rediss-cluster - TLS connected to a cluster

The URLs must have the following format:

One node

Text
1redis|rediss :// [[username:]password@] host [:port][/database]

Example: redis://localhost:6379

Clustered

Text
1redis|rediss[-cluster] :// [[username:]password@] host [:port][?[node=host1:port1][&node=host2:port2][&node=hostN:portN]]

or, if configured with multiple URLs:

YAML
1[
2  "redis|rediss[-cluster] :// [[username:]password@] host [:port]",
3  "redis|rediss[-cluster] :// [[username:]password@] host1 [:port1]",
4  "redis|rediss[-cluster] :// [[username:]password@] host2 [:port2]",
5]

TLS and authentication

For Redis TLS connections, you can set up a client certificate or override the root certificate authority by configuring tls in your router's YAML config file.

YAML
1redis:
2  urls: ["rediss://redis.example.com:6379"]
3  tls:
4    certificate_authorities: ${file./path/to/ca.crt}
5    client_authentication:
6      certificate_chain: ${file./path/to/certificate_chain.pem}
7      key: ${file./path/to/key.pem}

For comprehensive TLS configuration details, including client-side and subgraph TLS, see TLS configuration.

note
Nest this tls configuration under response_cache.subgraph.all for subgraphs or response_cache.connector.all for Apollo Connectors — both use the same Redis configuration type, TLS included.

Timeout configuration

Redis connections and commands have default timeouts that you can override. The timeouts are tailored to specific caching operations: fetch, insert, invalidate, and maintenance. Set a lower fetch timeout and a higher insert timeout for a good balance of reliability and client responsiveness:

YAML
router.yaml
1response_cache:
2  enabled: true
3  subgraph:
4    all:
5      enabled: true
6      redis:
7        urls: ["redis://..."]
8        fetch_timeout: 250ms
9        insert_timeout: 750ms
10        invalidate_timeout: 750ms

TTL for cache entries

The ttl option defines the default global expiration for cache entries. You must set a TTL for all enabled subgraphs.

To prevent potential cache overflow, consider setting the TTL to 24 hours or twice the median publish interval (whichever is less), and monitor cache utilization in your environment, especially if you cache a lot of different data:

YAML
router.yaml
1response_cache:
2  enabled: true
3  subgraph:
4    all:
5      enabled: true
6      ttl: 24h
7    products:
8      enabled: true
9      ttl: 6h

For connector sources, configure TTL with connector.all.ttl or per source. Connectors respect Cache-Control headers from the upstream HTTP response directly—the configured TTL is used as a fallback when no header is present:

YAML
router.yaml
1response_cache:
2  enabled: true
3  connector:
4    all:
5      enabled: true
6      ttl: 24h
7      cache_key_headers: [] # Required: request headers to include in the cache key ([] = none)
8    sources:
9      "products.rest_api":
10        ttl: 6h

Namespace prefix

When using the same Redis instance for multiple purposes, the namespace option defines a prefix for all the keys defined by the router:

YAML
router.yaml
1response_cache:
2  enabled: true
3  subgraph:
4    all:
5      enabled: true
6      redis:
7        urls: ["redis://..."]
8        namespace: response_cache

Required to start

When active, the required_to_start option prevents the router from starting if it can't connect to Redis. By default, the router starts without a connection to Redis, which means it sends requests to subgraphs directly, bypassing the cache:

YAML
router.yaml
1response_cache:
2  enabled: true
3  subgraph:
4    all:
5      enabled: true
6      redis:
7        urls: ["redis://..."]
8        required_to_start: true

Connection pool size

The pool_size option defines the number of Redis connections the router opens. By default, the router opens five connections. If you have high traffic between the router and Redis, or if you observe latency in those requests, increase the pool size to reduce that latency. You can measure whether you need to increase this number by monitoring the apollo.router.operations.response_cache.insert and apollo.router.operations.response_cache.fetch response cache metrics. If the time to insert and fetch data from the cache is long, it might be because of delays in acquiring a connection from the pool:

YAML
router.yaml
1response_cache:
2  enabled: true
3  subgraph:
4    all:
5      enabled: true
6      redis:
7        urls: ["redis://..."]
8        pool_size: 15

Suppressing the Cache-Control client response header

By default, router includes a Cache-Control header in the supergraph response sent to clients, reflecting the computed cache policy—for example, Cache-Control: max-age=300, public.

Set include_cache_control_header_on_router_response: false to suppress this header when:

  • Router sits behind a CDN or reverse proxy that manages caching headers

  • Downstream clients misinterpret Cache-Control headers from router

  • You want internal Redis caching active while preventing clients from caching responses

YAML
router.yaml
1response_cache:
2  enabled: true
3  include_cache_control_header_on_router_response: false
4  subgraph:
5    all:
6      enabled: true
7      redis:
8        urls: ["redis://..."]

When include_cache_control_header_on_router_response is false, router omits the Cache-Control header from client responses. All internal caching behavior—Redis storage, TTL enforcement, cache key computation, and the cache debugger—remains unchanged.