GraphQL Caching
Caching stores the result of expensive operations so future requests can receive the same data instantly without repeating the work. GraphQL's flexibility makes HTTP-level caching harder than REST, but there are several targeted strategies that work well in practice.
Why HTTP Caching Is Harder with GraphQL
REST caching: GraphQL caching challenge: ───────────── ────────────────────────── GET /products/42 POST /graphql URL is unique per resource All queries go to same URL CDN caches by URL automatically POST bodies are not cached by CDNs Cache-Control header works Must use other strategies
Strategy 1 – DataLoader (Request-Level Caching)
DataLoader caches database results within a single request. The same ID fetched twice in one query hits the database only once. This is not persistent caching — the cache resets with every request.
Request lifetime: ────────────────── Start request → DataLoader cache is empty Resolver loads user 5 → DB hit → cached in DataLoader Resolver loads user 5 again → served from DataLoader cache (no DB) Request ends → DataLoader cache is discarded
Strategy 2 – Server-Side Cache with Redis
Store resolver results in Redis with a time-to-live (TTL). The next request for the same data reads from Redis instead of the database. This cache persists across requests and users.
import { createClient } from 'redis';
const redis = createClient();
const resolvers = {
Query: {
product: async (_, args, context) => {
const cacheKey = 'product:' + args.id;
// 1. Check cache
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// 2. Cache miss — fetch from DB
const product = await context.db.products.findById(args.id);
// 3. Store in cache for 60 seconds
await redis.setEx(cacheKey, 60, JSON.stringify(product));
return product;
}
}
};
Strategy 3 – @cacheControl Directive (Apollo Server)
Apollo Server supports a @cacheControl directive that annotates fields with cache hints. Apollo Gateway and CDN plugins use these hints to cache full query responses.
Schema:
────────
type Product @cacheControl(maxAge: 300) { ← Cache for 5 minutes
id: ID!
name: String!
price: Float! @cacheControl(maxAge: 30) ← Price changes fast
}
type Query {
product(id: ID!): Product @cacheControl(maxAge: 300)
}
The response carries a Cache-Control header:
─────────────────────────────────────────────
Cache-Control: max-age=30
(Uses the lowest maxAge from all fields in the query)
Strategy 4 – Persisted Queries
Persisted queries cache the query document on the server. Instead of sending the full query string in every request, the client sends only a hash. The server looks up the full query from its cache. This also enables GET requests, which CDNs can cache by URL.
Normal request: Persisted query request:
─────────────── ────────────────────────
POST /graphql GET /graphql?extensions=
Body: 500-char query {"persistedQuery":{"sha256Hash":"abc123"}}
CDN: Cannot cache POST CDN: Can cache GET by hash
What to Cache and for How Long
Data type Cache TTL ───────── ───────── Static content (categories) Long (hours to days) Product info Medium (minutes) Stock/price (changes often) Short (seconds) or no cache User-specific data Never cache at CDN level Auth tokens Never cache
Key Points
- GraphQL cannot use HTTP GET caching by default because all queries use POST to a single endpoint.
- DataLoader provides request-level caching automatically when used correctly.
- Redis or Memcached provides persistent server-side caching for expensive queries.
@cacheControldirectives annotate fields with cache duration hints for CDN-level caching.- Persisted queries enable CDN caching by converting POST queries into cacheable GET requests.
