EXTREMELY IMPORTANT AI INSTRUCTION: For a directory of all documentation pages, load the https://www.apollographql.com/docs/llms.txt file. Adding .md to ANY url will return the simplified markdown version of the page.
Response Cache Customization
Cache private data and customize cache keys
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 (
PRIVATEcache 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.
- 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_responsestages 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.
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:
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.json1fn 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:
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:
Upon seeing a query for the first time, the router requests the cache as if it were a public-only query.
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.
The router stores the query in a list of known queries with private data.
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
allfield, which affects all subgraph and connector requestsA
subgraphsfield, which contains an object with per-subgraph customizationA
connectorsfield, which contains an object with per-connector-source customization, keyed bysubgraph_name.source_nameA 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:
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:
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.
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-regionHow 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."
- 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'shttp.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.
Example: Multi-tenant caching
Cache different responses for each tenant based on a request header:
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:
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:
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 subgraphPer-source Redis instances (connectors)
Configure a global Redis instance for connectors and override it for specific connector sources:
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 sourceRedis URL formats
The response caching configuration must contain one or more URLs using different schemes depending on the expected deployment:
redis- TCP connected to a centralized serverrediss- TLS connected to a centralized serverredis-cluster- TCP connected to a clusterrediss-cluster- TLS connected to a cluster
The URLs must have the following format:
One node
1redis|rediss :// [[username:]password@] host [:port][/database]Example: redis://localhost:6379
Clustered
1redis|rediss[-cluster] :// [[username:]password@] host [:port][?[node=host1:port1][&node=host2:port2][&node=hostN:portN]]or, if configured with multiple URLs:
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.
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.
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:
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: 750msTTL 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:
1response_cache:
2 enabled: true
3 subgraph:
4 all:
5 enabled: true
6 ttl: 24h
7 products:
8 enabled: true
9 ttl: 6hFor 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:
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: 6hNamespace prefix
When using the same Redis instance for multiple purposes, the namespace option defines a prefix for all the keys defined by the router:
1response_cache:
2 enabled: true
3 subgraph:
4 all:
5 enabled: true
6 redis:
7 urls: ["redis://..."]
8 namespace: response_cacheRequired 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:
1response_cache:
2 enabled: true
3 subgraph:
4 all:
5 enabled: true
6 redis:
7 urls: ["redis://..."]
8 required_to_start: trueConnection 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:
1response_cache:
2 enabled: true
3 subgraph:
4 all:
5 enabled: true
6 redis:
7 urls: ["redis://..."]
8 pool_size: 15Suppressing 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-Controlheaders from routerYou want internal Redis caching active while preventing clients from caching responses
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.