Migrate to GraphQL Yoga v5
Response Caching
Response caching is a technique for reducing server load by caching GraphQL query operation results. For incoming GraphQL Query operations with the same variable values, the same response is returned from a cache instead of executed again.
Quick Start
The response cache is a separate package that needs to be installed.
yarn add @graphql-yoga/plugin-response-cache
The following sample setup show as slow field resolver (Query.slow
).
import { createYoga, createSchema } from 'graphql-yoga'
import { createServer } from 'node:http'
import { useResponseCache } from '@graphql-yoga/plugin-response-cache'
const yoga = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
slow: String
}
`,
resolvers: {
Query: {
slow: async () => {
await new Promise((resolve) => setTimeout(resolve, 5000))
return 'I am slow.'
}
}
}
}),
plugins: [
useResponseCache({
// global cache
session: () => null
})
]
})
const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})
After starting the server we can execute a GraphQL Query operation, that selects the Query.slow
field.
curl -X POST -H 'Content-Type: application/json' http://localhost:4000/graphql \
-d '{"query":"{slow}"}' -w '\nTotal time : %{time_total}'
The output will look similar to the following:
{"data":{"slow":"I am slow."}}
Total time:5.026632
After executing the same curl statement a second time, the duration is significantly lower.
{"data":{"slow":"I am slow."}}
Total time:0.007571%
Session based caching
If your GraphQL API returns specific data depending on the viewer’s session, you can use the session
option to cache the response per session.
Usually, the session is determined by an HTTP header, e.g. an user id within the encoded access token.
The session
function receives a request
parameter that is a Request
object.
useResponseCache({
// cache based on the authentication header
session: (request) => request.headers.get('authentication')
})
Time to Live (TTL)
It is possible to give cached operations a time to live. Either globally, based on schema coordinates or object types.
If a query operation result contains multiple objects of the same or different types, the lowest TTL is picked.
useResponseCache({
session: () => null,
// by default cache all operations for 2 seconds
ttl: 2_000,
ttlPerType: {
// only cache query operations containing User for 500ms
User: 500
},
ttlPerSchemaCoordinate: {
// cache operations selecting Query.lazy for 10 seconds
'Query.lazy': 10_000
}
})
Invalidations via Mutation
When executing a mutation operation the cached query results that contain type entities within the Mutation result will be automatically be invalidated.
mutation UpdateUser {
updateUser(id: 1, newName: "John") {
__typename
id
name
}
}
{
"data": {
"updateLaunch": {
"__typename": "User",
"id": "1",
"name": "John"
}
}
}
For the given GraphQL operation and execution result all cached query results that contain the type User
with the id 1
will be invalidated.
This behavior can be disabled by setting the invalidateViaMutation
option to false
.
useResponseCache({
session: (request) => null,
invalidateViaMutation: false
})
Manual Invalidation
You can invalidate a type or specific instances of a type using the cache invalidation API.
In order to use the API, you need to manually instantiate the cache an pass it to the useResponseCache
plugin.
import {
useResponseCache,
createInMemoryCache
} from '@graphql-yoga/plugin-response-cache'
const cache = createInMemoryCache()
useResponseCache({
session: () => null,
cache
})
Then in your business logic you can call the invalidate
method on the cache instance.
Invalidate all GraphQL query results that reference a specific type:
cache.invalidate([{ type: 'User' }])
Invalidate all GraphQL query results that reference a specific entity of a type:
cache.invalidate([{ type: 'User', id: '1' }])
Invalidate all GraphQL query results multiple entities in a single call.
cache.invalidate([
{ type: 'Post', id: '1' },
{ type: 'User', id: '2' }
])
External Cache
By default, the response cache stores all the cached query results in memory.
If you want a cache that is shared between multiple server instances you can use the Redis cache implementation, which is available as a separate package.
yarn add @envelop/response-cache-redis
import { useResponseCache } from '@graphql-yoga/plugin-response-cache'
import { createRedisCache } from '@envelop/response-cache-redis'
import Redis from 'ioredis'
const redis = new Redis({
host: 'my-redis-db.example.com',
port: '30652',
password: '1234567890'
})
const redis = new Redis('redis://:1234567890@my-redis-db.example.com:30652')
const cache = createRedisCache({ redis })
useResponseCache({
session: () => null,
cache
})
HTTP Caching via ETag
and If-None-Match
headers
Response Caching plugin sends ETag
headers to the client, and respects If-None-Match
headers in the HTTP request.
If the client sends an If-None-Match
header with the same value as the ETag
header, the server will respond with a 304 Not Modified
status code without any content, which allows you to reduce the server load.
Most of the browsers and some HTTP clients support this behavior, so you can use it to improve the performance of your frontend application.
Learn more about ETag
and If-None-Match
headers.
Example with curl
First we send a request to the GraphQL server, and we can see that the response contains the headers
curl -H 'Content-Type: application/json' \
"http://localhost:4000/graphql?query={me{id name}}" -v
Then the server will respond a data something the following with the ETag
and Last-Modified
headers:
ETag
is the key that is used to identify the cached response.Last-Modified
is used to determine if the cached response is still valid.
> GET /graphql?query={me{id,name}} HTTP/1.1
> Host: localhost:4000
> User-Agent: curl/7.68.0
> Accept: application/json
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< access-control-allow-origin: *
< content-length: 130
< content-type: application/json; charset=utf-8
< etag: 2c0ebfe7b2b0273029f2fa23a99d213b56f4838756b3ef7b323c04de1e836be3
< last-modified: Wed Feb 15 2023 15:23:55 GMT+0300 (GMT+03:00)
< Date: Wed, 15 Feb 2023 12:23:55 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
{"data":{"me":{"id":"1","name":"Bob"}}}
In the next calls, we can use the ETag
header as the If-None-Match
header together with Last-Modified
header as If-Modified-Since
to check if the cached response is still valid.
curl -H "Accept: application/json" \
-H "If-None-Match: 2c0ebfe7b2b0273029f2fa23a99d213b56f4838756b3ef7b323c04de1e836be3" \
-H "If-Modified-Since: Wed Feb 15 2023 15:23:55 GMT" \
"http://localhost:4000/graphql?query=\{me\{id,name\}\}" -v
Then the server will return 304: Not Modified
status code with no content.