Server-side caching
Configure caching behavior on a per-field basis
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.
You can define field-level cache settings statically in your schema definition (as shown above) or dynamically in your resolvers.
Field settings
When caching operation results, 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. You can specify these details statically in your schema definition or dynamically in your resolvers.
In your schema (static)
Apollo Server defines 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.
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. |
Use @cacheControl
for fields that should always be cached with the same settings. If caching settings might change at runtime, instead 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
:
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 a
Post
'svotes
field is cached for a maximum of 30 seconds. - The value of a
Post
'sreadByCurrentUser
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:
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 Post
s), 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.
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 the same fields as the @cacheControl
directive.
If you're using TypeScript, you need to add the following
import
statement to indicate that theinfo
parameter includes acacheControl
field:import 'apollo-cache-control';
Default maxAge
By default, the following schema fields have a maxAge
of 0
(meaning their values are not cached unless you specify otherwise):
- All root fields (i.e., the fields of the
Query
andMutation
objects) - Fields that return an object or interface type
Scalar fields inherit their default cache behavior (including maxAge
) from their parent object type. This enables you to define cache behavior for most scalars at the type level, while overriding that behavior in individual cases at the field level.
As a result of these defaults, no schema fields are cached by default.
Setting the default maxAge
You can set a default maxAge
(instead of 0
) that's applied to every field that doesn't specify a different value.
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
in the ApolloServer
constructor, like so:
const server = new ApolloServer({
// ...other options...
cacheControl: {
defaultMaxAge: 5, // 5 seconds
},
}));
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, private
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).
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, but this is less secure because your query string and GraphQL variables are sent as plaintext URL query parameters.
Disabling Cache-Control
You can prevent Apollo Server from setting Cache-Control
headers by setting calculateHttpHeaders
to false
in the ApolloServer
constructor:
const server = new ApolloServer({
// ...other options...
cacheControl: {
calculateHttpHeaders: false,
},
}));
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-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 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
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 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 . |