Response Cache Customization

Cache private data and customize cache keys

Requires ≥ Router v2.8.0
Preview Feature

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
Response caching is happening at the subgraph service level and this plugin is happening right after coprocessor and Rhai plugins which means you'll always call the coprocessor or Rhai script even if the response is cached (both at request and response level).

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
1preview_response_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}

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 requests

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

  • 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}

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}

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
2preview_response_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

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. For example:

YAML
router.yaml
1# Enable response caching globally
2preview_response_cache:
3  enabled: true
4  subgraph:
5    all:
6      enabled: true
7      # Configure Redis globally
8      redis:
9        urls: [ "rediss://redis.example.com:6379" ]
10        username: root
11        password: ${env.REDIS_PASSWORD}
12        tls:
13          certificate_authorities: ${file./path/to/ca.crt}
14          client_authentication:
15            certificate_chain: ${file./path/to/certificate_chain.pem}
16            key: ${file./path/to/key.pem}

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
1preview_response_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
1preview_response_cache:
2  enabled: true
3  subgraph:
4    all:
5      enabled: true
6      ttl: 24h
7    products:
8      enabled: true
9      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
1preview_response_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
1preview_response_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
1preview_response_cache:
2  enabled: true
3  subgraph:
4    all:
5      enabled: true
6      redis:
7        urls: ["redis://..."]
8        pool_size: 15
Feedback

Edit on GitHub

Ask Community