Skip to content

Commit 33cfafc

Browse files
sarahxsandersbenjie
authored andcommitted
docs: add guide on caching strategies (#4411)
Adds guide on caching strategies --------- Co-authored-by: Benjie <[email protected]>
1 parent bcc8505 commit 33cfafc

File tree

3 files changed

+302
-0
lines changed

3 files changed

+302
-0
lines changed

cspell.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ words:
118118
- ruru
119119
- oneof
120120
- vercel
121+
- unbatched
121122

122123
# used as href anchors
123124
- graphqlerror
@@ -173,5 +174,7 @@ words:
173174
- XXXF
174175
- bfnrt
175176
- wrds
177+
- overcomplicating
178+
- cacheable
176179
- pino
177180
- debuggable

website/pages/docs/_meta.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const meta = {
2727
'custom-scalars': '',
2828
'advanced-custom-scalars': '',
2929
'n1-dataloader': '',
30+
'caching-strategies': '',
3031
'resolver-anatomy': '',
3132
'graphql-errors': '',
3233
'using-directives': '',
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
---
2+
title: Caching Strategies
3+
---
4+
5+
# Caching Strategies
6+
7+
Caching is a core strategy for improving the performance and scalability of GraphQL
8+
servers. Because GraphQL allows clients to specify exactly what they need, the server often
9+
does more work per request (but fewer requests) compared to many other APIs.
10+
11+
This guide explores different levels of caching in a GraphQL.js so you can apply the right
12+
strategy for your application.
13+
14+
## Why caching matters
15+
16+
GraphQL servers commonly face performance bottlenecks due to repeated fetching of the
17+
same data, costly resolver logic, or expensive database queries. Since GraphQL shifts
18+
much of the composition responsibility to the server, caching becomes essential for maintaining
19+
fast response times and managing backend load.
20+
21+
## Caching levels
22+
23+
There are several opportunities to apply caching within a GraphQL server:
24+
25+
- **Resolver-level caching**: Cache the result of specific fields.
26+
- **Request-level caching**: Batch and cache repeated access to backend resources
27+
within a single operation.
28+
- **Operation result caching**: Reuse the entire response for repeated identical queries.
29+
- **Schema caching**: Cache the compiled schema when startup cost is high.
30+
- **Transport/middleware caching**: Leverage caching behavior in HTTP servers or proxies.
31+
32+
Understanding where caching fits in your application flow helps you apply it strategically
33+
without overcomplicating your system.
34+
35+
## Resolver-level caching
36+
37+
Resolver-level caching is useful when a specific field’s value is expensive to compute and
38+
commonly requested with the same arguments. Instead of recomputing or refetching the data on
39+
every request, you can store the result temporarily in memory and return it directly when the
40+
same input appears again.
41+
42+
### Use cases
43+
44+
- Fields backed by slow or rate-limited APIs
45+
- Fields that require complex computation
46+
- Data that doesn't change frequently
47+
48+
For example, consider a field that returns information about a product:
49+
50+
```js
51+
// utils/cache.js
52+
import LRU from 'lru-cache';
53+
54+
export const productCache = new LRU({ max: 1000, ttl: 1000 * 60 }); // 1 min TTL
55+
```
56+
57+
The next example shows how to use that cache inside a resolver to avoid repeated database
58+
lookups:
59+
60+
```js
61+
// resolvers/product.js
62+
import { productCache } from '../utils/cache.js';
63+
64+
export const resolvers = {
65+
Query: {
66+
product(_, { id }, context) {
67+
const cached = productCache.get(id);
68+
if (cached) return cached;
69+
70+
const productPromise = context.db.products.findById(id);
71+
productCache.set(id, productPromise);
72+
return productPromise;
73+
},
74+
},
75+
};
76+
```
77+
78+
This example uses [`lru-cache`](https://www.npmjs.com/package/lru-cache), which limits the
79+
number of stored items and support TTL-based expiration. You can replace it with Redis or
80+
another cache if you need cross-process consistency.
81+
82+
### Guidelines
83+
84+
- Resolver-level caches are global. Be careful with authorization-sensitive data.
85+
- This technique works best for data that doesn't change often or can tolerate short-lived
86+
staleness.
87+
- TTL should match how often the underlying data is expected to change.
88+
89+
## Request-level caching with DataLoader
90+
91+
[DataLoader](https://github.com/graphql/dataloader) is a utility for batching and caching
92+
backend access during a single GraphQL operation. It's designed to solve the N+1 problem,
93+
where the same resource is fetched repeatedly in a single query across multiple fields.
94+
95+
### Use cases
96+
97+
- Resolving nested relationships
98+
- Avoiding duplicate database or API calls
99+
- Scoping caching to a single request, without persisting globally
100+
101+
The following example defines a DataLoader instance that batches user lookups by ID:
102+
103+
```js
104+
// loaders/userLoader.js
105+
import DataLoader from 'dataloader';
106+
import { batchGetUsers } from '../services/users.js';
107+
108+
export const createUserLoader = () => new DataLoader(ids => batchGetUsers(ids));
109+
```
110+
111+
You can then include the loader in the per-request context to isolate it from other
112+
operations:
113+
114+
```js
115+
// context.js
116+
import { createUserLoader } from './loaders/userLoader.js';
117+
118+
export function createContext() {
119+
return {
120+
userLoader: createUserLoader(),
121+
};
122+
}
123+
```
124+
125+
Finally, use the loader in your resolvers to batch-fetch users efficiently:
126+
127+
```js
128+
// resolvers/user.js
129+
export const resolvers = {
130+
Query: {
131+
async users(_, __, context) {
132+
return context.userLoader.loadMany([1, 2, 3]);
133+
},
134+
},
135+
};
136+
```
137+
138+
### Guidelines
139+
140+
- The cache is scoped to the request. Each request gets a fresh loader instance.
141+
- This strategy works best for resolving repeated references to the same resource type.
142+
- This isn't a long-lived cache. Combine it with other layers for broader coverage.
143+
144+
To read more about DataLoader and the N+1 problem,
145+
see [Solving the N+1 Problem with DataLoader](https://github.com/graphql/dataloader).
146+
147+
## Operation result caching
148+
149+
Operation result caching stores the complete response of a query, keyed by the query string, variables, and potentially
150+
HTTP headers. It can dramatically improve performance when the same query is sent frequently, particularly for read-heavy
151+
applications.
152+
153+
### Use cases
154+
155+
- Public data or anonymous content
156+
- Expensive queries that return stable results
157+
- Scenarios where the same query is sent frequently
158+
159+
The following example defines two functions to interact with a Redis cache,
160+
storing and retrieving cached results:
161+
162+
```js
163+
// cache/queryCache.js
164+
import Redis from 'ioredis';
165+
const redis = new Redis();
166+
167+
export async function getCachedResponse(cacheKey) {
168+
const cached = await redis.get(cacheKey);
169+
return cached ? JSON.parse(cached) : null;
170+
}
171+
172+
export async function cacheResponse(cacheKey, result, ttl = 60) {
173+
await redis.set(cacheKey, JSON.stringify(result), 'EX', ttl);
174+
}
175+
```
176+
177+
The next example shows how to wrap your execution logic to check the cache first and store results
178+
afterward:
179+
180+
```js
181+
// graphql/executeWithCache.js
182+
import { getCachedResponse, cacheResponse } from '../cache/queryCache.js';
183+
184+
/**
185+
* Stores in-flight requests to executeWithCache such that concurrent
186+
* requests with the same cacheKey will only result in one call to
187+
* `getCachedResponse` / `cacheResponse`. Once a request completes
188+
* (with or without error) it is removed from the map.
189+
*/
190+
const inflight = new Map();
191+
192+
export function executeWithCache({ cacheKey, executeFn }) {
193+
const existing = inflight.get(cacheKey);
194+
if (existing) return existing;
195+
196+
const promise = _executeWithCacheUnbatched({ cacheKey, executeFn });
197+
inflight.set(cacheKey, promise);
198+
return promise.finally(() => inflight.delete(cacheKey));
199+
}
200+
201+
async function _executeWithCacheUnbatched({ cacheKey, executeFn }) {
202+
const cached = await getCachedResponse(cacheKey);
203+
if (cached) return cached;
204+
205+
const result = await executeFn();
206+
await cacheResponse(cacheKey, result);
207+
return result;
208+
}
209+
```
210+
211+
### Guidelines
212+
213+
- Don't cache personalized or auth-sensitive data unless you scope the cache per user or token.
214+
- Invalidation is nontrivial. Consider TTLs, cache versioning, or event-driven purging.
215+
216+
## Schema caching
217+
218+
Schema caching is useful when your schema construction is expensive, for example, when
219+
you are dynamically generating types, you are stitching multiple schemas, or fetching
220+
remote GraphQL services. This is especially important in serverless environments,
221+
where cold starts can significantly impact performance.
222+
223+
### Use cases
224+
225+
- Serverless functions that rebuild the schema on each invocation
226+
- Applications that use schema stitching or remote schema delegation
227+
- Environments where schema generation takes noticeable time on startup
228+
229+
The following example shows how to cache a schema in memory after the first build:
230+
231+
```js
232+
import { buildSchema } from 'graphql';
233+
234+
let cachedSchema;
235+
236+
export function getSchema() {
237+
if (!cachedSchema) {
238+
cachedSchema = buildSchema(schemaSDLString); // or makeExecutableSchema()
239+
}
240+
return cachedSchema;
241+
}
242+
```
243+
244+
## Cache invalidation
245+
246+
No caching strategy is complete without an invalidation plan. Cached data can
247+
become stale or incorrect, and serving outdated information can lead to bugs or a
248+
degraded user experience.
249+
250+
The following are common invalidation techniques:
251+
252+
- **TTL (time-to-live)**: Automatically expire cached items after a time window
253+
- **Manual purging**: Remove or refresh cache entries when related data is updated
254+
- **Key versioning**: Encode version or timestamp metadata into cache keys
255+
- **Stale-while-revalidate**: Serve stale data while refreshing it in the background
256+
257+
Design your invalidation strategy based on your data’s volatility and your clients’
258+
tolerance for staleness.
259+
260+
## Third-party and edge caching
261+
262+
While GraphQL.js does not include built-in support for third-party or edge caching, it integrates
263+
well with external tools and middleware that handle full response caching or caching by query
264+
signature.
265+
266+
### Use cases
267+
268+
- Serving public, cacheable content to unauthenticated users
269+
- Deploying behind a CDN or reverse proxy
270+
- Using a gateway service that supports persistent response caching
271+
272+
The following tools and layers are commonly used:
273+
274+
- Redis or Memcached for in-memory and cross-process caching
275+
- CDN-level caching for static, cache-friendly GraphQL queries
276+
- API gateways
277+
278+
### Guidelines
279+
280+
- Partition cache entries for personalized data using auth tokens or headers
281+
- Monitor cache hit/miss ratios to identify tuning opportunities
282+
- Consider varying cache strategy per query or operation type
283+
284+
## Client-side caching
285+
286+
GraphQL clients include sophisticated client-side caches that store
287+
normalized query results and reuse them across views or components. While this is out of scope for GraphQL.js
288+
itself, server-side caching should be designed with client behavior in mind.
289+
290+
### When to consider it in server design
291+
292+
- You want to avoid redundant server work for cold-started clients
293+
- You need consistency between server and client freshness guarantees
294+
- You're coordinating with clients that rely on local cache behavior
295+
296+
Server-side and client-side caches should align on freshness guarantees and invalidation
297+
behavior. If the client doesn't re-fetch automatically, server-side staleness
298+
may be invisible but impactful.

0 commit comments

Comments
 (0)