Server-Side Caching
Configure caching behavior on a per-field basis
Once enabled, Apollo Server lets you to define cache control settings (maxAge
and scope
) for each field in your schema:
1type Post {
2 id: ID!
3 title: String
4 author: Author
5 votes: Int @cacheControl(maxAge: 30)
6 comments: [Comment]
7 readByCurrentUser: Boolean! @cacheControl(maxAge: 10, scope: PRIVATE)
8}
When Apollo Server resolves an operation, it calculates the result's correct cache behavior based on the most restrictive settings among the result's fields. You can then use this calculation to support any form of cache implementation you want, such as by providing it to your CDN via a Cache-Control
header.
Setting cache hints
You can define field-level cache hints statically in your schema definition or dynamically in your resolvers (or both).
Note that when setting cache hints, it's important to understand:
Which fields of your schema can be cached safely
How long a cached value should remain valid
Whether a cached value is global or user-specific
These details can vary significantly, even among the fields of a single object type.
In your schema (static)
Apollo Server recognizes the @cacheControl
directive, which you can use in your schema to define caching behavior either for a single field, or for all fields that return a particular type.
To use the @cacheControl
directive, you must add the following definitions to your server's schema:
1enum CacheControlScope {
2 PUBLIC
3 PRIVATE
4}
5
6directive @cacheControl(
7 maxAge: Int
8 scope: CacheControlScope
9 inheritMaxAge: Boolean
10) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION
If you don't add these definitions, Apollo Server throws an Unknown directive "@cacheControl"
error on startup.
The @cacheControl
directive accepts the following arguments:
Name | Description |
---|---|
maxAge | The maximum amount of time the field's cached value is valid, in seconds. The default value is 0 , but you can set a different default. |
scope | If PRIVATE , the field's value is specific to a single user. The default value is PUBLIC . See also Identifying users for PRIVATE responses. |
inheritMaxAge | If true , this field inherits the maxAge of its parent field instead of using the default maxAge . Do not provide maxAge if you provide this argument. |
Use @cacheControl
for fields that should usually be cached with the same settings. If caching settings might change at runtime, you can use the dynamic method.
Important: Apollo Server assigns each GraphQL response a
maxAge
equal to the lowestmaxAge
among included fields. If any field has amaxAge
of0
, the response will not be cached at all.Similarly, Apollo Server sets a response's
scope
toPRIVATE
if any included field isPRIVATE
.
Field-level definitions
This example defines cache control settings for two fields of the Post
type: votes
and readByCurrentUser
:
1type Post {
2 id: ID!
3 title: String
4 author: Author
5 votes: Int @cacheControl(maxAge: 30)
6 comments: [Comment]
7 readByCurrentUser: Boolean! @cacheControl(maxAge: 10, scope: PRIVATE)
8}
In this example:
The value of the
votes
field is cached for a maximum of 30 seconds.The value of the
readByCurrentUser
field is cached for a maximum of 10 seconds, and its visibility is restricted to a single user.
Type-level definitions
This example defines cache control settings for all schema fields that return a Post
object:
1type Post @cacheControl(maxAge: 240) {
2 id: Int!
3 title: String
4 author: Author
5 votes: Int
6 comments: [Comment]
7 readByCurrentUser: Boolean!
8}
If another object type in this schema includes a field of type Post
(or a list of Post
s), that field's value is cached for a maximum of 240 seconds:
1type Comment {
2 post: Post! # Cached for up to 240 seconds
3 body: String!
4}
Note that field-level settings override type-level settings. In the following case, Comment.post
is cached for a maximum of 120 seconds, not 240 seconds:
1type Comment {
2 post: Post! @cacheControl(maxAge: 120)
3 body: String!
4}
In your resolvers (dynamic)
You can decide how to cache a particular field's result while you're resolving it. To support this, Apollo Server's cache control plugin provides a cacheControl
object in the info
parameter that's passed to every resolver.
If you set a field's cache hint in its resolver, it overrides any cache hint you provided in your schema.
cacheControl.setCacheHint
The cacheControl
object includes a setCacheHint
method, which you call like so:
1import { cacheControlFromInfo } from '@apollo/cache-control-types';
2
3const resolvers = {
4 Query: {
5 post: (_, { id }, _, info) => {
6 // Access ApolloServerPluginCacheControl's extension of the GraphQLResolveInfo object
7 const cacheControl = cacheControlFromInfo(info)
8 cacheControl.setCacheHint({ maxAge: 60, scope: 'PRIVATE' });
9 return find(posts, { id });
10 },
11 },
12};
The setCacheHint
method accepts an object with maxAge
and scope
fields.
cacheControl.cacheHint
This object represents the field's current cache hint. Its fields include the following:
The field's current
maxAge
andscope
(which might have been set statically)A
restrict
method, which is similar tosetCacheHint
but it can't relax existing hint settings:TypeScript1import { cacheControlFromInfo } from '@apollo/cache-control-types'; 2 3// Access ApolloServerPluginCacheControl's extension of the GraphQLResolveInfo object 4const cacheControl = cacheControlFromInfo(info) 5 6// If we call this first... 7cacheControl.setCacheHint({ maxAge: 60, scope: 'PRIVATE' }); 8 9// ...then this changes maxAge (more restrictive) but NOT scope (less restrictive) 10cacheControl.cacheHint.restrict({ maxAge: 30, scope: 'PUBLIC' });
cacheControl.cacheHintFromType
This method enables you to get the default cache hint for a particular object type. This can be useful when resolving a union or interface field, which might return one of multiple object types.
Calculating cache behavior
For security, each operation response's cache behavior is calculated based on the most restrictive cache hints among the result's fields:
The response's
maxAge
equals the lowestmaxAge
among all fields. If that value is0
, the entire result is not cached.The response's
scope
isPRIVATE
if any field'sscope
isPRIVATE
.
Default maxAge
By default, the following schema fields have a maxAge
of 0
if you don't specify one:
Root fields (i.e., the fields of the
Query
,Mutation
, andSubscription
types)Because every GraphQL operation includes a root field, this means that by default, no operation results are cached unless you set cache hints!
Fields that return a non-scalar type (object, interface, or union) or a list of non-scalar types.
You can customize this default.
All other schema fields (i.e., non-root fields that return scalar types) instead inherit their default maxAge
from their parent field.
Why are these the maxAge
defaults?
Our philosophy behind Apollo Server caching is that a response should only be considered cacheable if every part of that response opts in to being cacheable. At the same time, we don't think developers should have to specify cache hints for every single field in their schema.
So, we follow these heuristics:
Root field resolvers are extremely likely to fetch data (because these fields have no parent), so we set their default
maxAge
to0
to avoid automatically caching data that shouldn't be cached.Resolvers for other non-scalar fields (objects, interfaces, and unions) also commonly fetch data because they contain arbitrarily many fields. Consequently, we also set their default
maxAge
to0
.Resolvers for scalar, non-root fields rarely fetch data and instead usually populate data via the
parent
argument. Consequently, these fields inherit their defaultmaxAge
from their parent to reduce schema clutter.
Of course, these heuristics aren't always correct! For example, the resolver for a non-root scalar field might indeed fetch remote data. You can always set your own cache hint for any field with an undesirable default behavior.
Ideally, you can provide a maxAge
for every field with a resolver that actually fetches data from a data source (such as a database or REST API). Most other fields can then inherit their cache hint from their parent (fields with resolvers that don't fetch data less commonly have specific caching needs). For more on this, see Recommended starting usage.
Setting a different default maxAge
You can set a default maxAge
that's applied to fields that otherwise receive the default maxAge
of 0
.
You should identify and address all exceptions to your default
maxAge
before you enable it in production, but this is a great way to get started with cache control.
Set your default maxAge
by passing the cache control plugin to the ApolloServer
constructor, like so:
1import { ApolloServerPluginCacheControl } from '@apollo/server/plugin/cacheControl';
2
3const server = new ApolloServer({
4 // ...other options...
5 plugins: [ApolloServerPluginCacheControl({ defaultMaxAge: 5 })], // 5 seconds
6});
Recommended starting usage
You usually don't need to specify cache hints for every field in your schema. Instead, we recommend doing the following as a starting point:
For fields that should never be cached, explicitly set
maxAge
to0
.Set a
maxAge
for every field with a resolver that actually fetches data from a data source (such as a database or REST API). You can base the value ofmaxAge
on the frequency of updates that are made to the relevant data.Set
inheritMaxAge: true
for each other non-root field that returns a non-scalar type.Note that you can only set
inheritMaxAge
statically.
Example maxAge
calculations
Consider the following schema:
1type Query {
2 book: Book
3 cachedBook: Book @cacheControl(maxAge: 60)
4 reader: Reader @cacheControl(maxAge: 40)
5}
6
7type Book {
8 title: String
9 cachedTitle: String @cacheControl(maxAge: 30)
10}
11
12type Reader {
13 book: Book @cacheControl(inheritMaxAge: true)
14}
Let's look at some queries and their resulting maxAge
values:
1# maxAge: 0
2# Query.book doesn't set a maxAge and it's a root field (default 0).
3query GetBookTitle {
4 book { # 0
5 cachedTitle # 30
6 }
7}
8
9# maxAge: 60
10# Query.cachedBook has a maxAge of 60, and Book.title is a scalar, so it
11# inherits maxAge from its parent by default.
12query GetCachedBookTitle {
13 cachedBook { # 60
14 title # inherits
15 }
16}
17
18# maxAge: 30
19# Query.cachedBook has a maxAge of 60, but Book.cachedTitle has
20# a maxAge of 30.
21query GetCachedBookCachedTitle {
22 cachedBook { # 60
23 cachedTitle # 30
24 }
25}
26
27# maxAge: 40
28# Query.reader has a maxAge of 40. Reader.Book is set to
29# inheritMaxAge from its parent, and Book.title is a scalar
30# that inherits maxAge from its parent by default.
31query GetReaderBookTitle {
32 reader { # 40
33 book { # inherits
34 title # inherits
35 }
36 }
37}
Using with Federation
Using cache control with Apollo Federation requires v0.1.0 of
@apollo/subgraph
(previously v0.28 of@apollo/federation
) in your subgraph, v0.36 of@apollo/gateway
in your Gateway, and v3.0.2 of Apollo Server in both servers.
When using Apollo Federation, the @cacheControl
directive and CacheControlScope
enum may be defined in a subgraph's schema. An Apollo Server-based subgraph will calculate and set the cache hint for the response that it sends to the gateway as it would for a non-federated Apollo Server sending a response to a client. The gateway will then calculate the cache hint for the overall response based on the most restrictive settings among all of the responses received from the subgraphs involved in query plan execution.
Setting entity cache hints
Subgraph schemas contain an _entities
root field on the Query
type, so all query plans that require entity resolution will have a maxAge
of 0
set by default. To override this default behavior, you can add a @cacheControl
directive to an entity's definition:
1type Book @key(fields: "isbn") @cacheControl(maxAge: 30) {
2 isbn: String!
3 title: String
4}
When the _entities
field is resolved it will check the applicable concrete type for a cache hint (which would be the Book
type in the example above) and apply that hint instead.
To set cache hints dynamically, the cacheControl
object and its methods are also available in the info
parameter of the __resolveReference
resolver.
Overriding subgraph cache hints in the gateway
If a subgraph does not specify a max-age
, the gateway will assume its response (and
in turn, the overall response) cannot be cached. To override this behavior, you can set the Cache-Control
header in the didReceiveResponse
method of a RemoteGraphQLDataSource
.
Additionally, if the gateway should ignore Cache-Control
response headers from subgraphs that will affect the operation's cache policy, then you can set the honorSubgraphCacheControlHeader
property of a RemoteGraphQLDataSource
to false
(this value is true
by default):
1const gateway = new ApolloGateway({
2 // ...
3 buildService({ url }) {
4 return new RemoteGraphQLDataSource({
5 url,
6 honorSubgraphCacheControlHeader: false;
7 });
8 }
9});
The effect of setting honorSubgraphCacheControlHeader
to false
is to have no impact on the cacheability of the response in either direction. In other words, this property won’t determine whether the response can be cached, but it does exclude a subgraph's Cache-Control
header from consideration in the gateway's calculation. If all subgraphs are excluded from consideration when calculating the overall Cache-Control
header, the response sent to the client will not be cached.
Caching with a CDN
By default, Apollo Server sends a Cache-Control
header with all responses that describes the response's cache policy.
When the response is cacheable, the header has this format:
1Cache-Control: max-age=60, private
When the response is not cacheable, the header has the value Cache-Control: no-store
.
To be cacheable, all of the following must be true:
The operation has a non-zero
maxAge
.The operation has a single response rather than an incremental delivery response.
There are no errors in the response.
If you run Apollo Server behind a CDN or another caching proxy, you can configure it to use this header's value to cache responses appropriately. See your CDN's documentation for details (for example, here's the documentation for Amazon CloudFront).
Some CDNs require custom headers for caching or custom values in the cache-control
header like s-maxage
. You can configure your ApolloServer
instance accordingly by telling the built-in cache control plugin to just calculate a policy without setting HTTP headers, and specifying your own plugin:
1new ApolloServer({
2 plugins: [
3 ApolloServerPluginCacheControl({ calculateHttpHeaders: false }),
4 {
5 async requestDidStart() {
6 return {
7 async willSendResponse(requestContext) {
8 const { response, overallCachePolicy } = requestContext;
9 const policyIfCacheable = overallCachePolicy.policyIfCacheable();
10 if (policyIfCacheable && !response.headers && response.http) {
11 response.http.headers.set(
12 'cache-control',
13 // ... or the values your CDN recommends
14 `max-age=0, s-maxage=${
15 overallCachePolicy.maxAge
16 }, ${policyIfCacheable.scope.toLowerCase()}`,
17 );
18 }
19 },
20 };
21 },
22 },
23 ],
24});
Using GET requests
Because CDNs and caching proxies only cache GET requests (not POST requests, which Apollo Client sends for all operations by default), we recommend enabling automatic persisted queries and the useGETForHashedQueries
option in Apollo Client.
Alternatively, you can set the useGETForQueries
option of HttpLink in your ApolloClient
instance. However, most browsers enforce a size limit on GET requests, and large query strings might exceed this limit.
Disabling cache control
You can prevent Apollo Server from setting Cache-Control
headers by installing the ApolloServerPluginCacheControl
plugin yourself and setting calculateHttpHeaders
to false
:
1import { ApolloServerPluginCacheControl } from '@apollo/server/plugin/cacheControl';
2
3const server = new ApolloServer({
4 // ...other options...
5 plugins: [ApolloServerPluginCacheControl({ calculateHttpHeaders: false })],
6});
If you do this, the cache control plugin still calculates caching behavior for each operation response. You can then use this information with other plugins (like the response cache plugin).
To disable cache control calculations entirely, instead install the ApolloServerPluginCacheControlDisabled
plugin (this plugin has no effect other than preventing the cache control plugin from being installed):
1import { ApolloServerPluginCacheControlDisabled } from '@apollo/server/plugin/disabled';
2
3const server = new ApolloServer({
4 // ...other options...
5 plugins: [ApolloServerPluginCacheControlDisabled()],
6});
Caching with responseCachePlugin
(advanced)
You can cache Apollo Server query responses in stores like Redis, Memcached, or Apollo Server's default in-memory cache. For more information, see Configuring cache backends.
In-memory cache setup
To set up your in-memory response cache, you first import the responseCachePlugin
and provide it to the ApolloServer
constructor:
1import responseCachePlugin from '@apollo/server-plugin-response-cache';
2
3const server = new ApolloServer({
4 // ...other options...
5 plugins: [responseCachePlugin()],
6});
On initialization, this plugin automatically begins caching responses according to field settings.
The plugin uses the same in-memory LRU cache as Apollo Server's other features. For environments with multiple server instances, you might instead want to use a shared cache backend, such as Memcached or Redis.
In addition to the
Cache-Control
HTTP header, theresponseCachePlugin
also sets theAge
HTTP header to the number of seconds the returned value has been in the cache.
Memcached/Redis setup
See Configuring external caching.
You can also implement your own cache backend.
Identifying users for PRIVATE
responses
If a cached response has a PRIVATE
scope, its value is accessible by only a single user. To enforce this restriction, the cache needs to know how to identify that user.
To enable this identification, you provide a sessionId
function to your responseCachePlugin
, like so:
1import responseCachePlugin from '@apollo/server-plugin-response-cache';
2const server = new ApolloServer({
3 // ...other settings...
4 plugins: [
5 responseCachePlugin({
6 sessionId: (requestContext) =>
7 requestContext.request.http.headers.get('session-id') || null,
8 }),
9 ],
10});
Important: If you don't define a
sessionId
function,PRIVATE
responses are not cached at all.
The cache uses the return value of this function to identify the user who can later access the cached PRIVATE
response. In the example above, the function uses a session-id
header from the original operation request.
If a client later executes the exact same query and has the same identifier, Apollo Server returns the PRIVATE
cached response if it's still available.
Separating responses for logged-in and logged-out users
By default, PUBLIC
cached responses are accessible by all users. However, if you define a sessionId
function (as shown above), Apollo Server caches up to two versions of each PUBLIC
response:
One version for users with a null
sessionId
One version for users with a non-null
sessionId
This enables you to cache different responses for logged-in and logged-out users. For example, you might want your page header to display different menu items depending on a user's logged-in status.
Configuring reads and writes
In addition to the sessionId
function, you can provide the following functions to your responseCachePlugin
to configure cache reads and writes. Each of these functions takes a GraphQLRequestContext
(representing the incoming operation) as a parameter.
Function | Description |
---|---|
extraCacheKeyData | This function's return value (any JSON-stringifiable object) is added to the key for the cached response. For example, if your API includes translatable text, this function can return a string derived from requestContext.request.http.headers.get('accept-language') . |
shouldReadFromCache | If this function returns false , Apollo Server skips the cache for the incoming operation, even if a valid response is available. |
shouldWriteToCache | If this function returns false , Apollo Server doesn't cache its response for the incoming operation, even if the response's maxAge is greater than 0 . |
generateCacheKey | Customize generation of the cache key. By default, this is the SHA256 hash of the JSON encoding of an object containing relevant data. |