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:

GraphQL
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:

GraphQL
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:

NameDescription
maxAgeThe 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.
scopeIf PRIVATE, the field's value is specific to a single user. The default value is PUBLIC. See also Identifying users for PRIVATE responses.
inheritMaxAgeIf 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 lowest maxAge among included fields. If any field has a maxAge of 0, the response will not be cached at all.

Similarly, Apollo Server sets a response's scope to PRIVATE if any included field is PRIVATE.

Field-level definitions

This example defines cache control settings for two fields of the Post type: votes and readByCurrentUser:

GraphQL
schema.graphql
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:

GraphQL
schema.graphql
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 Posts), that field's value is cached for a maximum of 240 seconds:

GraphQL
schema.graphql
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:

GraphQL
schema.graphql
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:

TypeScript
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 and scope (which might have been set statically)

  • A restrict method, which is similar to setCacheHint but it can't relax existing hint settings:

    TypeScript
    1import { 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 lowest maxAge among all fields. If that value is 0, the entire result is not cached.

  • The response's scope is PRIVATE if any field's scope is PRIVATE.

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, and Subscription 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 to 0 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 to 0.

  • Resolvers for scalar, non-root fields rarely fetch data and instead usually populate data via the parent argument. Consequently, these fields inherit their default maxAge 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:

TypeScript
1import { ApolloServerPluginCacheControl } from '@apollo/server/plugin/cacheControl';
2
3const server = new ApolloServer({
4  // ...other options...
5  plugins: [ApolloServerPluginCacheControl({ defaultMaxAge: 5 })],  // 5 seconds
6});

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 to 0.

  • 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 of maxAge 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:

GraphQL
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:

GraphQL
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:

GraphQL
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):

TypeScript
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:

Text
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:

TypeScript
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:

TypeScript
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):

TypeScript
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:

TypeScript
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, the responseCachePlugin also sets the Age 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:

TypeScript
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.

FunctionDescription
extraCacheKeyDataThis 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').
shouldReadFromCacheIf this function returns false, Apollo Server skips the cache for the incoming operation, even if a valid response is available.
shouldWriteToCacheIf 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.
generateCacheKeyCustomize generation of the cache key. By default, this is the SHA256 hash of the JSON encoding of an object containing relevant data.
Feedback

Edit on GitHub

Forums