Response Caching Quickstart
Get started with Redis-backed response caching
Learn how GraphOS Router can cache portions of query responses using Redis to improve your query latency in the supergraph.
Quickstart
Follow this guide to enable and add a minimal configuration for response caching in GraphOS Router.
Prerequisites
To use response caching in GraphOS Router, you must set up:
A Redis instance or cluster that your router instances can communicate with
A router that connects to GraphOS
Configure router for response caching
In router.yaml, configure preview_response_cache:
Enable response caching globally
Configure Redis using the same conventions described in distributed caching
Configure response caching per subgraph, with overrides per subgraph for disabling response caching and TTL
For example:
1# Enable response caching globally
2preview_response_cache:
3 enabled: true
4 debug: true # Enable the ability to return data to the cache debugger. Avoid enabling this in production.
5 invalidation:
6 listen: 0.0.0.0:4000
7 path: /invalidation
8 subgraph:
9 all:
10 enabled: true
11 # Configure Redis for all subgraphs
12 redis:
13 urls: ["redis://localhost:6379"]
14 invalidation:
15 enabled: true
16 shared_key: ${env.INVALIDATION_SHARED_KEY} # Use environment variable INVALIDATION_SHARED_KEY
17 # Configure overrides for specific subgraphs
18 subgraphs:
19 inventory:
20 enabled: false # Disable caching for inventory subgraph
21 products:
22 redis:
23 urls: ["redis://products-cache:6379"] # Use different Redis for productsIdentify what data to cache
To identify which subgraphs would benefit most from caching, you can enable metrics and increase their granularity. Keep in mind that more granularity leads to higher metric cardinality, which might increase costs in your APM.
Configure this metric as follows:
1telemetry:
2 instrumentation:
3 instruments:
4 cache: # Cache instruments configuration
5 apollo.router.operations.response.cache: # A counter which counts the number of cache hit and miss for subgraph requests
6 attributes:
7 graphql.type.name: true # Include the entity type name. default: false
8 subgraph.name: # Custom attributes to include the subgraph name in the metric
9 subgraph_name: true
10 # supergraph.operation.name: # Add custom attribute to display the supergraph operation name
11 # supergraph_operation_name: string
12 # You can add more custom attributes using subgraph selectorsYou can now use the apollo.router.operations.response.cache metric to create a dashboard like this example:
The left chart shows cache hits per subgraph and type. No cache hits appear because your subgraphs don't return any Cache-Control headers. The right chart shows cache misses and potential cache hits. In this example, caching the User type from the posts subgraph would be beneficial given the high number of cache misses.
Integrate with your schema
Consider an example with two subgraphs: users and posts.
1extend schema
2 @link(
3 url: "https://specs.apollo.dev/federation/v2.12"
4 import: ["@key", "@external"]
5 )
6
7type Query {
8 user(id: ID!): User
9 users: [User!]!
10}
11
12type User @key(fields: "id") {
13 id: ID!
14 name: String!
15 email: String!
16 posts: [Post!]! @external
17}
18
19type Post @key(fields: "id") {
20 id: ID!
21 content: String! @external
22}1extend schema
2 @link(url: "https://specs.apollo.dev/federation/v2.12", import: ["@key"])
3
4type Query {
5 posts: [Post!]
6 post(id: ID!): Post
7}
8
9type User @key(fields: "id") {
10 id: ID!
11 posts: [Post!]!
12}
13
14type Post @key(fields: "id") {
15 id: ID!
16 title: String!
17 content: String!
18 author: User!
19 featuredImage: String
20}Based on the metrics, caching the User type on the posts subgraph would provide significant benefits. Enable this using the @cacheControl directive.
@cacheControl only works if you're using a subgraph which supports @cacheControl, like Apollo Server. If you're using another server, update your codebase to return the correct Cache-Control header using the method provided by that server according to its documentation.Here is the new version of the schema for subgraph posts:
1extend schema
2 @link(url: "https://specs.apollo.dev/federation/v2.12", import: ["@key"])
3
4enum CacheControlScope {
5 PUBLIC
6 PRIVATE
7}
8directive @cacheControl(
9 maxAge: Int
10 scope: CacheControlScope
11 inheritMaxAge: Boolean
12) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION
13
14type Query {
15 posts: [Post!] @cacheControl(maxAge: 60)
16 post(id: ID!): Post
17}
18
19type User @key(fields: "id") @cacheControl(maxAge: 60) {
20 id: ID!
21 posts: [Post!]!
22}
23
24type Post @key(fields: "id") @cacheControl(maxAge: 60) {
25 id: ID!
26 title: String!
27 content: String!
28 author: User!
29 featuredImage: String
30}The cache control is set with a TTL of 60 seconds, so both the Post and User types are cached for 60 seconds.
With caching enabled, you can see the difference in the dashboard: more cache hits and fewer cache misses.
Response times are also faster, as shown in the right panel of this screenshot:
If you execute an example query twice in Apollo Sandbox with cache debugger enabled you should see all the entries coming from cache in source column:
Invalidation
With cached data based on TTL, you might want to increase these TTLs and automatically invalidate specific data when you know it has changed. Response caching provides multiple ways to invalidate data. For more details, see Invalidation. This quickstart uses cache tags, which work like surrogate cache keys for REST APIs. You can tag data in your schema to enable targeted invalidation.
Use the @cacheTag directive introduced in Federation v2.12. It takes a format argument to create your cache tag. For the User type and the posts root field, create different cache tags:
1extend schema
2 @link(url: "https://specs.apollo.dev/federation/v2.12", import: ["@key", "@cacheTag"])
3
4enum CacheControlScope {
5 PUBLIC
6 PRIVATE
7}
8directive @cacheControl(
9 maxAge: Int
10 scope: CacheControlScope
11 inheritMaxAge: Boolean
12) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION
13
14type Query {
15 posts: [Post!] @cacheControl(maxAge: 60) @cacheTag(format: "posts")
16 post(id: ID!): Post
17}
18
19type User @key(fields: "id") @cacheControl(maxAge: 60) @cacheTag(format: "user-{$key.id}") @cacheTag(format: "user") {
20 id: ID!
21 posts: [Post!]!
22}
23
24type Post @key(fields: "id") @cacheControl(maxAge: 60) {
25 id: ID!
26 title: String!
27 content: String!
28 author: User!
29 featuredImage: String
30}rover after making these changes.The User type is tagged with a static cache tag format user and a dynamic one with variable interpolation using the entity key id to generate the cache tag. For example, if the fetched User has an id of 42, it generates user-42 as a cache tag.
With tagged data, you can invalidate it using a curl command to invalidate the user with ID 42:
1curl --request POST \
2 --header "authorization: $INVALIDATION_SHARED_KEY" \
3 --header 'content-type: application/json' \
4 --url http://localhost:4000/invalidation \
5 --data '[{"kind":"cache_tag","subgraphs":["posts"],"cache_tag":"user-42"}]'INVALIDATION_SHARED_KEY is an environment variable containing a token for authenticating requests to this endpoint. This was configured in the router setup at the beginning of this page.
The call returns the number of invalidated entries. For example, if only one entry was invalidated:
1{
2 "count": 1
3}If you execute an example query twice in Apollo Sandbox with cache debugger enabled you should see all the entries coming from cache in source column and now you should see the generated cache tags for entities and root field posts in Cache tags column:
To explore response caching and see what data has been cached in a query, use the cache debugger.