Server-side caching
Configure caching behavior on a per-field basis
New in Apollo Server 3: You must manually define the
@cacheControldirective in your schema to use static cache hints. See below.
Note: Apollo Federation doesn't currently support @cacheControl out-of-the-box. There is an issue on the Federation repo which discusses this and proposes possible workarounds.
Apollo Server enables you to define cache control settings (maxAge and scope) for each field in your schema:
type Post {
id: ID!
title: String
author: Author
votes: Int @cacheControl(maxAge: 30) comments: [Comment]
readByCurrentUser: Boolean! @cacheControl(maxAge: 10, scope: PRIVATE)}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:
enum CacheControlScope {
PUBLIC
PRIVATE
}
directive @cacheControl(
maxAge: Int
scope: CacheControlScope
inheritMaxAge: Boolean
) on FIELD_DEFINITION | OBJECT | INTERFACE | UNIONIf 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
maxAgeequal to the lowestmaxAgeamong included fields. If any field has amaxAgeof0, the response will not be cached at all.Similarly, Apollo Server sets a response's
scopetoPRIVATEif any included field isPRIVATE.
Field-level definitions
This example defines cache control settings for two fields of the Post type: votes and readByCurrentUser:
type Post {
id: ID!
title: String
author: Author
votes: Int @cacheControl(maxAge: 30) comments: [Comment]
readByCurrentUser: Boolean! @cacheControl(maxAge: 10, scope: PRIVATE)}In this example:
- The value of the
votesfield is cached for a maximum of 30 seconds. - The value of the
readByCurrentUserfield 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:
type Post @cacheControl(maxAge: 240) { id: Int!
title: String
author: Author
votes: Int
comments: [Comment]
readByCurrentUser: Boolean!
}If another object type in this schema includes a field of type Post (or a list of Posts), that field's value is cached for a maximum of 240 seconds:
type Comment {
post: Post! # Cached for up to 240 seconds
body: String!
}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:
type Comment {
post: Post! @cacheControl(maxAge: 120)
body: String!
}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 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:
const resolvers = {
Query: {
post: (_, { id }, _, info) => {
info.cacheControl.setCacheHint({ maxAge: 60, scope: 'PRIVATE' }); return find(posts, { id });
}
}
}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
maxAgeandscope(which might have been set statically) -
A
restrictmethod, which is similar tosetCacheHintbut it can't relax existing hint settings:// If we call this first... info.cacheControl.setCacheHint({ maxAge: 60, scope: 'PRIVATE' }); // ...then this changes maxAge (more restrictive) but NOT scope (less restrictive) info.cacheControl.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
maxAgeequals the lowestmaxAgeamong all fields. If that value is0, the entire result is not cached. - The response's
scopeisPRIVATEif any field'sscopeisPRIVATE.
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, andSubscriptiontypes)- 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
maxAgeto0to 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
maxAgeto0. - Resolvers for scalar, non-root fields rarely fetch data and instead usually populate data via the
parentargument. Consequently, these fields inherit their defaultmaxAgefrom 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
maxAgebefore 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:
import { ApolloServerPluginCacheControl } from 'apollo-server-core';
const server = new ApolloServer({
// ...other options...
plugins: [ApolloServerPluginCacheControl({ defaultMaxAge: 5 })], // 5 seconds
}));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
maxAgeto0. - Set a
maxAgefor 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 ofmaxAgeon the frequency of updates that are made to the relevant data. -
Set
inheritMaxAge: truefor each other non-root field that returns a non-scalar type.- Note that you can only set
inheritMaxAgestatically.
- Note that you can only set
Example maxAge calculations
Consider the following schema:
type Query {
book: Book
cachedBook: Book @cacheControl(maxAge: 60)
reader: Reader @cacheControl(maxAge: 40)
}
type Book {
title: String
cachedTitle: String @cacheControl(maxAge: 30)
}
type Reader {
book: Book @cacheControl(inheritMaxAge: true)
}Let's look at some queries and their resulting maxAge values:
# maxAge: 0
# Query.book doesn't set a maxAge and it's a root field (default 0).
query GetBookTitle {
book { # 0
cachedTitle # 30
}
}
# maxAge: 60
# Query.cachedBook has a maxAge of 60, and Book.title is a scalar, so it
# inherits maxAge from its parent by default.
query GetCachedBookTitle {
cachedBook { # 60
title # inherits
}
}
# maxAge: 30
# Query.cachedBook has a maxAge of 60, but Book.cachedTitle has
# a maxAge of 30.
query GetCachedBookCachedTitle {
cachedBook { # 60
cachedTitle # 30
}
}
# maxAge: 40
# Query.reader has a maxAge of 40. Reader.Book is set to
# inheritMaxAge from its parent, and Book.title is a scalar
# that inherits maxAge from its parent by default.
query GetReaderBookTitle {
reader { # 40
book { # inherits
title # inherits
}
}
}Caching with a CDN
Whenever Apollo Server sends an operation response that has a non-zero maxAge, it includes a Cache-Control HTTP header that describes the response's cache policy.
The header has this format:
Cache-Control: max-age=60, privateIf 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).
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:
import { ApolloServerPluginCacheControl } from 'apollo-server-core';
const server = new ApolloServer({
// ...other options...
plugins: [ApolloServerPluginCacheControl({ calculateHttpHeaders: false })],
}));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):
import { ApolloServerPluginCacheControlDisabled } from 'apollo-server-core';
const server = new ApolloServer({
// ...other options...
plugins: [ApolloServerPluginCacheControlDisabled()],
}));Caching with responseCachePlugin (advanced)
You can cache Apollo Server query responses in stores like Redis, Memcached, or Apollo Server's in-memory cache.
In-memory cache setup
To set up your in-memory response cache, you first import the responseCachePlugin and provide it to the ApolloServer constructor:
import responseCachePlugin from 'apollo-server-plugin-response-cache';
const server = new ApolloServer({
// ...other options...
plugins: [responseCachePlugin()],
});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-ControlHTTP header, theresponseCachePluginalso sets theAgeHTTP header to the number of seconds the returned value has been in the cache.
Memcached/Redis setup
See Using Memcached/Redis as a cache storage backend.
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:
import responseCachePlugin from 'apollo-server-plugin-response-cache';
const server = new ApolloServer({
// ...other settings...
plugins: [responseCachePlugin({
sessionId: (requestContext) => (requestContext.request.http.headers.get('sessionid') || null),
})],
});Important: If you don't define a
sessionIdfunction,PRIVATEresponses 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 sessionid 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. |