Skip to content

Commit

Permalink
Caching edits ready for review
Browse files Browse the repository at this point in the history
  • Loading branch information
Stephen Barlow authored and Stephen Barlow committed Mar 4, 2021
1 parent 8a08ed6 commit fd4eb93
Showing 1 changed file with 107 additions and 51 deletions.
158 changes: 107 additions & 51 deletions docs/source/performance/caching.md
@@ -1,39 +1,44 @@
---
title: Caching
description: Control server-side caching behavior on a per-field basis
title: Server-side caching
sidebar_title: Caching
description: Configure caching behavior on a per-field basis
---

Caching query results in Apollo Server can significantly improve response times for commonly executed queries.
You can cache Apollo Server query results in stores like Redis, Memcached, or an in-process cache. This can significantly improve response times for commonly executed queries.

However, when caching results, it's important to understand:

* Which fields of your schema can be cached
* 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 between fields of a single type.
These details can vary significantly, even among the fields of a single object type.

Apollo Server enables you to define cache control settings _per schema field_. You can do this [statically in your schema definition](#in-your-schema-static), or [dynamically in your resolvers](#in-your-resolvers-dynamic). Apollo Server combines _all_ of your defined settings to power its caching features, including:
Apollo Server enables you to define cache control settings _per schema field_. You can do this [statically in your schema definition](#in-your-schema-static), or [dynamically in your resolvers](#in-your-resolvers-dynamic).

* HTTP caching headers for CDNs and browsers
* A GraphQL full responses cache
After you define these settings, Apollo Server can use them to [cache results in stores](#caching-a-response) like Redis or Memcached, or to [provide `Cache-Control` headers to your CDN](#serving-http-cache-headers-for-cdns).

## Control settings
> Apollo Server never caches empty responses or responses that contain GraphQL errors.

## Field settings

### In your schema (static)

Apollo Server defines the `@cacheControl` directive, which you can use in your schema to define caching behavior for a single field, or for _all_ fields that return a particular _type_.
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.

This 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](#setting-a-default-maxage). |
| `scope` | If `PRIVATE`, cached values are specific to a single user. The default value is `PUBLIC`. |
| `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](#setting-the-default-maxage). |
| `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](#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](#in-your-resolvers-dynamic).

#### Field-level settings
> **Important:** Apollo Server assigns each GraphQL response a `maxAge` according to the _lowest_ `maxAge` among included fields. For details, see [Response-level caching](#response-level-caching).
#### Field-level

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

Expand All @@ -44,16 +49,16 @@ type Post {
author: Author
votes: Int @cacheControl(maxAge: 30)
comments: [Comment]
readByCurrentUser: Boolean! @cacheControl(scope: PRIVATE)
readByCurrentUser: Boolean! @cacheControl(maxAge: 10, scope: PRIVATE)
}
```

In this example:

* The value of a `Post`'s `votes` field is cached for a maximum of 30 seconds.
* The value of a `Post`'s `readByCurrentUser` field can be cached _indefinitely_, but its visibility is limited to a single user.
* The value of a `Post`'s `readByCurrentUser` field is cached for a maximum of 10 seconds, and its visibility is restricted to a single user.

#### Type-level settings
#### Type-level

This example defines cache control settings for _all_ schema fields that return a `Post` object:

Expand All @@ -68,7 +73,7 @@ type Post @cacheControl(maxAge: 240) {
}
```

If this schema also defines a type with a field that returns a `Post` (or a list of `Post`s), that field's value is cached for a maximum of 240 seconds:
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:

```graphql:title=schema.graphql
type Comment {
Expand All @@ -77,7 +82,7 @@ type Comment {
}
```

**Note that [field-level settings](#field-level-settings) override type-level settings.** In the following case, `Comment.post` is cached for 120 seconds, _not_ 240 seconds:
**Note that [field-level settings](#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:title=schema.graphql
type Comment {
Expand Down Expand Up @@ -112,82 +117,133 @@ The `setCacheHint` method accepts an object with the same fields as [the `@cache
> import 'apollo-cache-control';
> ```
### Default behavior
### Default `maxAge`

By default, the following schema fields have a `maxAge` of `0` (meaning they are _not_ cached unless you specify otherwise):
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` and `Mutation` objects)
* All **root fields** (i.e., the fields of the `Query` and `Mutation` 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](#type-level-settings), while overriding that behavior in individual cases at the [field level](#field-level-settings).

#### Setting a default `maxAge`
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.

The power of cache hints comes from being able to set them precisely to different values on different types and fields based on your understanding of your implementation's semantics. But when getting started with the cache control API, you might just want to apply the same `maxAge` to most of your resolvers.
> 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.
You can achieve this by specifying a default max age when you create your `ApolloServer`. This max age will be used instead of 0 for root, object, and interface fields which don't explicitly set `maxAge` via schema hints (including schema hints on the type that they return) or the dynamic API. You can override this for a particular resolver or type by setting `@cacheControl(maxAge: 0)`. For example:
Set your default `maxAge` in the `ApolloServer` constructor, like so:

```javascript
const server = new ApolloServer({
// ...
// ...other options...
cacheControl: {
defaultMaxAge: 5,
defaultMaxAge: 5, // 5 seconds
},
}));
```

### The overall cache policy
## Response-level caching

Apollo Server's cache API lets you declare fine-grained cache hints on specific resolvers. Apollo Server then combines these hints into an overall cache policy for the response. The `maxAge` of this policy is the minimum `maxAge` across all fields in your request. As [described above](#setting-a-default-maxage), the default `maxAge` of all root fields and non-scalar fields is 0, so the overall cache policy for a response will have `maxAge` 0 (ie, uncacheable) unless all root and non-scalar fields in the response have cache hints (or if `defaultMaxAge` is specified).
Although you configure caching behavior per _schema field_, Apollo Server caches data per _operation response_. In other words, the entirety of a GraphQL operation's response is cached as a single entity.

If the overall cache policy has a non-zero `maxAge`, its scope is `PRIVATE` if any hints have scope `PRIVATE`, and `PUBLIC` otherwise.
Because of this, Apollo Server uses the following logic to calculate a response's cache behavior:

## Serving HTTP cache headers
* The response's `maxAge` is equal to the _lowest_ `maxAge` among _all_ fields included in the response.
* Consequently, if _any_ queried field has a `maxAge` of `0`, the entire response is _not cached_.
* If _any_ queried field has a `scope` of `PRIVATE`, the _entire response_ is considered `PRIVATE` (i.e., restricted to a single user). Otherwise, it's considered `PUBLIC`.

For any response whose overall cache policy has a non-zero `maxAge`, Apollo Server will automatically set the `Cache-Control` HTTP response header to an appropriate value describing the `maxAge` and scope, such as `Cache-Control: max-age=60, private`. If you run your Apollo Server instance behind a [CDN](https://en.wikipedia.org/wiki/Content_delivery_network) or other caching proxy, it can use this header's value to know how to cache your GraphQL responses.
## Setting up the cache

As many CDNs and caching proxies only cache GET requests (not POST requests) and may have a limit on the size of a GET URL, you may find it helpful to use [automatic persisted queries](https://github.com/apollographql/apollo-link-persisted-queries), especially with the `useGETForHashedQueries` option to `apollo-link-persisted-queries`.

If you don't want to set HTTP cache headers, pass `cacheControl: {calculateHttpHeaders: false}` to `new ApolloServer()`.

## Saving full responses to a cache

Apollo Server lets you save cacheable responses to a Redis, Memcached, or in-process cache. Cached responses respect the `maxAge` cache hint.

To use the response cache, you need to install its plugin when you create your `ApolloServer`:
To set up your cache, you first import the `responseCachePlugin` and provide it to the `ApolloServer` constructor:

```javascript
import responseCachePlugin from 'apollo-server-plugin-response-cache';

const server = new ApolloServer({
// ...
// ...other options...
plugins: [responseCachePlugin()],
});
```

By default, the response cache plugin will use the same cache used by other Apollo Server features, which defaults to an in-memory LRU cache. When running multiple server instances, you鈥檒l want to use a shared cache backend such as Memcached or Redis instead. See [the data sources documentation](/data/data-sources/#using-memcachedredis-as-a-cache-storage-backend) for details on how to customize Apollo Server's cache. If you want to use a different cache backend for the response cache than for other Apollo Server caching features, just pass a `KeyValueCache` as the `cache` option to the `responseCachePlugin` function.
This plugin uses the same in-memory LRU cache as Apollo Server's other features. For environments with multiple server instances, you should instead use a shared cache backend, such as Memcached or Redis. For details, see [Using Memcached/Redis as a cache storage backend](../data/data-sources/#using-memcachedredis-as-a-cache-storage-backend).

If you have data whose response should be cached separately for different users, set `@cacheControl(scope: PRIVATE)` hints on the data, and teach the cache control plugin how to tell your users apart by defining a `sessionId` hook:
> You can also [implement your own cache backend](../data/data-sources/#implementing-your-own-cache-backend).

### Identifying users for `PRIVATE` responses

If a cached response has a [`PRIVATE` scope](#in-your-schema-static), 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:

```javascript
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),
})],
});
```

Responses whose overall cache policy scope is `PRIVATE` are shared only among sessions with the same session ID. Private responses are not cached if the `sessionId` hook is not defined or returns 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](#identifying-users-for-private-responses)), 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`**

Responses whose overall cache policy scope is `PUBLIC` are shared separately among all sessions with `sessionId` null and among all sessions with non-null `sessionId`. Caching these separately allows you to have different caches for all logged-in users vs all logged-out users, if there is easily cacheable data that should only be visible to logged-in users.
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.

Responses containing GraphQL errors or no data are never cached.
### Configuring reads and writes

The plugin allows you to define a few more hooks to affect cache behavior for a specific request. All hooks take in a `GraphQLRequestContext`.
In addition to [the `sessionId` function](#identifying-users-for-private-responses), 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.

- `extraCacheKeyData`: this hook can return any JSON-stringifiable object which is added to the cache key. For example, if your API includes translatable text, this hook can return a string derived from `requestContext.request.http.headers.get('Accept-Language')`.
- `shouldReadFromCache`: if this hook returns false, the plugin will not read responses from the cache.
- `shouldWriteToCache`: if this hook returns false, the plugin will not write responses to the cache.
| 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`. |

In addition to the [`Cache-Control` HTTP header](#serving-http-cache-headers), the response cache plugin will also set the `Age` HTTP header to the number of seconds the value has been sitting in the cache.


## HTTP response headers

### `Age`

When Apollo Server returns a cached response to a client, the `responseCachePlugin` adds an `Age` header to the response. The header's value is the number of seconds the response has been in the cache.

### `Cache-Control` (for CDNs)

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. For example:

```
Cache-Control: max-age=60, private
```

If you run Apollo Server behind a CDN or another caching proxy, it can use this header's value to cache responses appropriately.

> Because CDNs and caching proxies only cache GET requests (not POST requests), we recommend using [automatic persisted queries](./apq/) with the [`useGETForHashedQueries` option](./apq/#) enabled.
#### Disabling `Cache-Control`

You can prevent Apollo Server from setting `Cache-Control` headers by setting `calculateHttpHeaders` to `false` in the `ApolloServer` constructor:

```js
const server = new ApolloServer({
// ...other options...
cacheControl: {
calculateHttpHeaders: false,
},
}));
```

0 comments on commit fd4eb93

Please sign in to comment.