Caching with Apollo Server
A Guide to Using ApolloServerPluginCacheControl
and Redis
Apollo Server provides multiple ways to implement caching, helping reduce server load and improve response times for frequently requested data. This document outlines how caching works in Apollo Server and how to use the ApolloServerPluginCacheControl
and responseCachePlugin
to store cache in Redis.
1. How Caching Works in Apollo Server
Apollo Server supports two types of caching:
- In-memory cache: Data is temporarily stored on the server to avoid recomputation on subsequent identical queries.
- External cache (e.g., Redis): Data is stored in external caching layers, such as Redis, allowing caching across multiple servers.
To enable caching, Apollo Server relies on two key plugins:
ApolloServerPluginCacheControl
: Controls caching at the field level using cache directives and provides metadata likemaxAge
(TTL) for each field or query.responseCachePlugin
: Caches full query responses based on the operation (i.e., the entire query and its result).
2. Caching with ApolloServerPluginCacheControl
ApolloServerPluginCacheControl
allows you to define cache control settings directly in your GraphQL schema using the @cacheControl
directive or globally through server settings.
Key Features:
maxAge
: Sets the time (in seconds) for which a response or field can be cached.scope
: Defines if the cache is shared (public
) or user-specific (private
).
Example:
type Query {
favouriteBooks: [Book] @cacheControl(maxAge: 60, scope: Private)
books: [Book] @cacheControl(maxAge: 60)
bookByTitle(title: String!): Book @cacheControl(maxAge: 30)
}
In this example, the books
query response is cached for 60 seconds, while bookByTitle
is cached for 30 seconds, both of them will be cached
once whereas the favouriteBooks
due to its private scope will be cached differently against each user request.
Default Cache Control:
If no @cacheControl
directive is used, you can specify a default maxAge
globally.
PS: We do not use Global Max Age
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginCacheControl({
defaultMaxAge: 10, // Default cache duration of 10 seconds
}),
],
});
3. Using the responseCachePlugin
with Redis
The responseCachePlugin
is designed to cache entire query responses (not just field-level caching). By integrating Redis as the cache storage, responses can be cached externally and shared across multiple instances of Apollo Server.
Steps to Enable Response Caching in Redis:
Install Redis and Required Packages: Install Redis and its Node.js client (
ioredis
) to interact with the Redis server.npm install ioredis @apollo/server-plugin-response-cache
Setup Redis Client: Initialize a Redis client using
ioredis
and configure the cache storage.const Redis = require('ioredis');
const redis = new Redis();Configure the
responseCachePlugin
: Use theresponseCachePlugin
to store query results in Redis.const { ApolloServer } = require('@apollo/server');
const { responseCachePlugin } = require('@apollo/server-plugin-response-cache');
const server = new ApolloServer({
typeDefs,
resolvers,
cache: redisCache,
plugins: [
responseCachePlugin({
shouldReadFromCache: async (requestContext) => {
const { maxAge } = requestContext.overallCachePolicy || {};
return maxAge > 0; // Cache only if maxAge is set
},
shouldWriteToCache: async (requestContext) => {
const { maxAge } = requestContext.overallCachePolicy || {};
return maxAge > 0; // Write to cache only if maxAge is set
},
}),
],
});Server Configuration: Add both
ApolloServerPluginCacheControl
(for defining cache policies) andresponseCachePlugin
(for full-response caching) to your Apollo Server configuration.const { ApolloServerPluginCacheControl } = require('@apollo/server-plugin-cache-control');
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginCacheControl({
}),
responseCachePlugin({
...pluginOptions
}),
],
});
4. How the Caching Workflow Operates
- Field-level Cache Control: The
ApolloServerPluginCacheControl
checks for@cacheControl
directives or uses the defaultmaxAge
to define TTLs for each query/field. - Full Response Caching: The
responseCachePlugin
caches the full result of a query if the request is cacheable (i.e., ifmaxAge
is set). - Redis as Cache Store: Query responses are stored in Redis, and when a cached query is requested again, the response is served from Redis, avoiding the need for computation.
5. Cache Invalidation
As we know, Redis stores cached data with a TTL (Max Age) value, and it automatically expires once that time limit is reached. However, in scenarios where the actual data changes before the cache expires, we need a way to invalidate the cache on demand to ensure the application always returns up-to-date information.
To address this, we’ve developed a custom plugin called invalidateCachePlugin
. This plugin introduces a custom directive that can be applied to mutations. With this directive, you can specify a list of queries to invalidate. Whenever the mutation runs successfully, the cache for all the listed queries is automatically cleared, ensuring the application serves fresh data.
Example:
type Mutation {
addToFavourites(id:ID!): Boolean @invalidateCache(queries:["favouriteBooks"])
addBook(book:BookInput!): [Book] @invalidateCache(queries: ["books"])
}
6 . Caveats
There is one weird issue happening with @cacheControl
directive, as it fails to set maxAge
for some queries.
To overcome that we've written a custom resolver for cacheController
to make sure that value is always there
Conclusion:
By using ApolloServerPluginCacheControl
for cache control metadata and responseCachePlugin
to cache full responses, you can significantly improve the performance of your Apollo Server application. Storing the cache in Redis ensures a scalable, centralized cache that works across multiple instances, making it ideal for distributed systems.