Docs
Launch GraphOS Studio

Server-side caching

Configure caching behavior on a per-field basis


Once enabled, lets you to define cache control settings (maxAge and scope) for each 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 resolves an , it calculates the result's correct cache behavior based on the most restrictive settings among the result's . 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 -level cache hints statically in your schema definition or dynamically in your (or both).

Note that when setting cache hints, it's important to understand:

  • Which 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 of a single .

In your schema (static)

recognizes the @cacheControl , which you can use in your schema to define caching behavior either for a single field, or for all 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 | UNION

If you don't add these definitions, throws an Unknown directive "@cacheControl" error on startup.

The @cacheControl accepts the following :

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 that should usually be cached with the same settings. If caching settings might change at runtime, you can use the dynamic method.

Important: assigns each response a maxAge equal to the lowest maxAge among included . If any field has a maxAge of 0, the response will not be cached at all.

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

Field-level definitions

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

schema.graphql
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 votes is cached for a maximum of 30 seconds.
  • The value of the readByCurrentUser 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 that return a Post object:

schema.graphql
type Post @cacheControl(maxAge: 240) {
id: Int!
title: String
author: Author
votes: Int
comments: [Comment]
readByCurrentUser: Boolean!
}

If another in this schema includes a of type Post (or a list of Posts), that 's value is cached for a maximum of 240 seconds:

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

schema.graphql
type Comment {
post: Post! @cacheControl(maxAge: 120)
body: String!
}

In your resolvers (dynamic)

You can decide how to cache a particular 's result while you're resolving it. To support this, provides a cacheControl object in the info parameter that's passed to every .

If you set a 's cache hint in its , 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 .

cacheControl.cacheHint

This object represents the 's current cache hint. Its fields include the following:

  • The '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:

    // 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 . This can be useful when resolving a union or interface , which might return one of multiple object types.

Calculating cache behavior

For security, each response's cache behavior is calculated based on the most restrictive cache hints among the result's :

  • The response's maxAge equals the lowest maxAge among all . If that value is 0, the entire result is not cached.
  • The response's scope is PRIVATE if any 's scope is PRIVATE.

Default maxAge

By default, the following schema have a maxAge of 0 if you don't specify one:

  • Root fields (i.e., the of the Query, Mutation, and Subscription types)
    • Because every includes a root , 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- types.

You can customize this default.

All other schema (i.e., non-root fields that return scalar types) instead inherit their default maxAge from their parent .

Why are these the maxAge defaults?

Our philosophy behind 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 in their schema.

So, we follow these heuristics:

  • Root 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.
  • for other non- (objects, interfaces, and unions) also commonly fetch data because they contain arbitrarily many . Consequently, we also set their default maxAge to 0.
  • for , non-root rarely fetch data and instead usually populate data via the parent . Consequently, these inherit their default maxAge from their parent to reduce schema clutter.

Of course, these heuristics aren't always correct! For example, the for a non-root 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 with a that actually fetches data from a (such as a database or REST API). Most other can then inherit their cache hint from their parent (fields with 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 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:

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

You usually don't need to specify cache hints for every in your schema. Instead, we recommend doing the following as a starting point:

  • For that should never be cached, explicitly set maxAge to 0.

  • Set a maxAge for every with a 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 that returns a non- type.

    • Note that you can only set inheritMaxAge statically.

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
}
}
}

Using with Federation

Using cache control with requires v0.1.0 of @apollo/subgraph (previously v0.28 of @apollo/federation) in your , v0.36 of @apollo/gateway in your Gateway, and v3.0.2 of in both servers.

When using Apollo Federation, the @cacheControl and CacheControlScope enum may be defined in a 's schema. An -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 execution.

Setting entity cache hints

contain an _entities root on the Query type, so all that require resolution will have a maxAge of 0 set by default. To override this default behavior, you can add a @cacheControl to an 's definition:

type Book @key(fields: "isbn") @cacheControl(maxAge: 30) {
isbn: String!
title: String
}

When the _entities 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 .

Overriding subgraph cache hints in the gateway

If a 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 that will affect the 's cache policy, then you can set the honorSubgraphCacheControlHeader property of a RemoteGraphQLDataSource to false (this value is true by default):

const gateway = new ApolloGateway({
// ...
buildService({ url }) {
return new RemoteGraphQLDataSource({
url,
honorSubgraphCacheControlHeader: false;
});
}
});

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 's Cache-Control header from consideration in the gateway's calculation. If all 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, 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:

Cache-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 has a non-zero maxAge.
  • The has a single response rather than an incremental delivery response.
  • There are no errors in the response.

If you run 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:

new ApolloServer({
plugins: [
ApolloServerPluginCacheControl({ calculateHttpHeaders: false }),
{
async requestDidStart() {
return {
async willSendResponse(requestContext) {
const { response, overallCachePolicy } = requestContext;
const policyIfCacheable = overallCachePolicy.policyIfCacheable();
if (policyIfCacheable && !response.headers && response.http) {
response.http.headers.set(
'cache-control',
// ... or the values your CDN recommends
`max-age=0, s-maxage=${
overallCachePolicy.maxAge
}, ${policyIfCacheable.scope.toLowerCase()}`,
);
}
},
};
},
},
],
});

Using GET requests

Because CDNs and caching proxies only cache GET requests (not POST requests, which sends for all by default), we recommend enabling automatic persisted queries and the useGETForHashedQueries option in .

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 strings might exceed this limit.

Disabling cache control

You can prevent from setting Cache-Control headers by installing the ApolloServerPluginCacheControl plugin yourself and setting calculateHttpHeaders to false:

import { ApolloServerPluginCacheControl } from '@apollo/server/plugin/cacheControl';
const server = new ApolloServer({
// ...other options...
plugins: [ApolloServerPluginCacheControl({ calculateHttpHeaders: false })],
});

If you do this, the cache control plugin still calculates caching behavior for each 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/plugin/disabled';
const server = new ApolloServer({
// ...other options...
plugins: [ApolloServerPluginCacheControlDisabled()],
});

Caching with responseCachePlugin (advanced)

You can cache 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:

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

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('session-id') || 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 session-id header from the original request.

If a client later executes the exact same and has the same identifier, 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), 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 ) 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.
Previous
Integration testing
Next
Cache backends
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company