GraphQL Caching Strategies — CDN, Resolver-Level, and Persisted Queries
In this tutorial, you will learn about Graphql Caching Strategies. We cover key concepts, practical examples, and best practices to help you master this topic.
GraphQL caching operates at multiple levels — HTTP caching for GET requests, resolver-level caching with DataLoader, persisted queries for repeatable operations, and CDN caching for public data — each addressing a different performance bottleneck.
What You'll Learn
- HTTP caching with GET queries and cache headers
- Apollo Server cache hints and automatic persisted queries
- DataLoader for batched and cached database access
- CDN caching for public GraphQL APIs
- Cache invalidation strategies
Why It Matters
Without caching, every GraphQL request hits your database and resolvers. Caching reduces latency from seconds to milliseconds and cuts database load by 90%+ for common queries. DodaTech's Durga Antivirus Pro caches device scan summaries for 5 minutes, serving 80% of dashboard requests from cache while still showing fresh threat data.
Real-World Use
A threat intelligence dashboard loads a list of recent threats. The query is the same for all users, so the API caches the response for 30 seconds. A user refreshes 10 times in 10 seconds — 9 requests hit the CDN cache, only 1 reaches the server.
flowchart LR
A["Client"] --> B["CDN Cache\n(public queries)"]
B --> C{"Cache Hit?"}
C -->|Yes| D["Return Cached Response"]
C -->|No| E["Apollo Server"]
E --> F["DataLoader Cache\n(per-request)"]
F --> G["Redis Cache\n(shared across servers)"]
G --> H["Database"]
style C fill:#fef3c7,stroke:#d97706
style D fill:#bbf7d0,stroke:#16a34a
Code Examples
Example 1: HTTP GET Caching with Apollo Server
const { ApolloServer } = require('apollo-server');
const responseCachePlugin = require('apollo-server-plugin-response-cache');
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [responseCachePlugin()],
cacheControl: {
defaultMaxAge: 5, // 5 seconds default
},
});
// Schema-level cache hints
const typeDefs = gql`
type Device @cacheControl(maxAge: 30) {
id: ID!
name: String!
os: String!
lastScan: ScanSummary @cacheControl(maxAge: 10)
}
type ScanSummary @cacheControl(maxAge: 60) {
status: String!
threats: Int!
lastChecked: String!
}
`;
Example 2: DataLoader for Batched Caching
const DataLoader = require('dataloader');
const createLoaders = (db) => ({
deviceById: new DataLoader(async (ids) => {
const devices = await db.devices
.find({ _id: { $in: ids } })
.toArray();
const map = new Map(devices.map(d => [d._id.toString(), d]));
return ids.map(id => map.get(id.toString()) || null);
}),
threatsByDeviceId: new DataLoader(async (deviceIds) => {
const threats = await db.threats
.find({ deviceId: { $in: deviceIds } })
.toArray();
const map = new Map();
threats.forEach(t => {
const list = map.get(t.deviceId.toString()) || [];
list.push(t);
map.set(t.deviceId.toString(), list);
});
return deviceIds.map(id => map.get(id.toString()) || []);
}, {
cacheKeyFn: key => key.toString(),
}),
});
const resolvers = {
Query: {
device: (_, { id }, { loaders }) => loaders.deviceById.load(id),
},
Device: {
threats: (device, _, { loaders }) =>
loaders.threatsByDeviceId.load(device.id),
},
};
Example 3: Automatic Persisted Queries (APQ)
const { ApolloServer } = require('apollo-server');
const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries: {
ttl: 900, // 15 minutes
},
});
// First request: sends full query + hash
// Subsequent requests: sends only hash
// Server looks up persisted query, executes, caches result
Common Mistakes
- Caching user-specific data — if devices contain user-specific fields, don't cache at the CDN. Use private cache control or vary by user ID.
- Over-caching list fields — lists with pagination cursors change frequently. Cache the individual items but not the list itself.
- Not invalidating on mutations — when a mutation creates a device, invalidate the device list and device detail caches.
- Using DataLoader without caching — DataLoader's per-request cache prevents duplicate loads within a single query but doesn't persist across requests.
- Ignoring cache-control headers from upstream services — if your database proxy sets cache headers, respect them rather than overriding.
Practice Questions
- What is the difference between public and private cache control in GraphQL?
- How does DataLoader batch multiple requests into a single database call?
- Why are persisted queries useful for caching?
- How do you invalidate cache after a mutation?
- What is the trade-off between cache TTL and data freshness?
Challenge: Design a multi-layer caching Strategy for a GraphQL API serving both authenticated dashboard users and a public API. Specify cache layers, TTLs, invalidation triggers, and how each layer handles user-specific vs public data.
Mini Project
Implement a GraphQL API with three caching layers: DataLoader for per-request batching, Redis for shared server cache (TTL 30s for devices, 60s for scan summaries), and CDN for public queries. Include automatic cache invalidation when mutations modify data.
FAQ
What's Next
Learn more about GraphQL performance optimization
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro