GraphQL Performance â Query Optimization, Caching and Best Practices
In this tutorial, you will learn about GraphQL Performance. We cover key concepts, practical examples, and best practices to help you master this topic.
GraphQL performance optimization requires strategies at every layer â resolver efficiency, response caching, CDN integration, query analysis, and database query optimization.
What You'll Learn
You will learn how to optimize GraphQL API performance with response caching, CDN integration, DataLoader patterns, persisted queries, query analysis, and monitoring.
Why Performance Matters
A slow GraphQL API undermines the developer experience and drives users away. Without optimization, complex dashboard queries take seconds instead of milliseconds. DodaTech's Durga Antivirus Pro dashboard was taking 3.2 seconds to load â after implementing response caching, DataLoader batching, and persisted queries, the same dashboard loads in 200ms.
flowchart LR
A["Request"] --> B{"Cached?"}
B -->|"Yes"| C["CDN Cache\n(5ms)"]
B -->|"No"| D["Apollo Cache\n(10ms)"]
D --> E{"Cache Hit?"}
E -->|"Yes"| F["Return cached"]
E -->|"No"| G["Execute Resolvers\n(optimized)"]
G --> H["DataLoader Batching\n(1 DB query)"]
H --> I["Response\n(50ms)"]
style C fill:#bbf7d0,stroke:#16a34a
style F fill:#bbf7d0,stroke:#16a34a
style I fill:#bbf7d0,stroke:#16a34a
style G fill:#fef3c7,stroke:#d97706
Prerequisites: Apollo Server, DataLoader, and caching concepts.
Response Caching with Cache Hints
# Schema cache hints
type Threat @cacheControl(maxAge: 60) {
id: ID!
name: String!
severity: Severity!
# Don't cache real-time status
status: ThreatStatus @cacheControl(maxAge: 0)
}
type Device @cacheControl(maxAge: 30) {
id: ID!
name: String!
os: String!
}
type Query {
# Cache public data for 5 minutes
threats: [Threat!]! @cacheControl(maxAge: 300)
# Cache device catalog for 30 seconds
devices: [Device!]! @cacheControl(maxAge: 30)
# Never cache user-specific data
myProfile: User! @cacheControl(maxAge: 0, scope: PRIVATE)
}
const { ApolloServer } = require('apollo-server');
const responseCachePlugin = require('apollo-server-plugin-response-cache');
const server = new ApolloServer({
typeDefs,
resolvers,
cache: 'bounded',
plugins: [
responseCachePlugin({
shouldReadFromCache: (requestContext) => {
// Only cache GET requests (CDN-friendly)
return requestContext.request.http?.method === 'GET';
},
sessionId: (requestContext) => {
const user = requestContext.context.user;
return user?.id || 'anonymous';
},
}),
],
});
CDN Caching with Persisted Queries
// Automatic Persisted Queries enable GET requests for caching
// CDN caches the response by URL
// URL includes the query hash, making it cacheable
// Server config
const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries: true,
cache: 'bounded',
responseCachePlugin(),
});
// Client sends GET request for cached queries
// GET /graphql?query={queryHash}&variables={...}
// CDN caches this URL for 5 minutes
// Subsequent requests served from CDN edge
DataLoader Optimization
// Common DataLoader patterns for performance
// 1. Primary loader â by ID
const userLoader = new DataLoader(ids =>
db.users.findByIds(ids).then(rows =>
ids.map(id => rows.find(r => r.id === id) || null)
),
{ maxBatchSize: 100 }
);
// 2. Relationship loader â one-to-many
const threatsByDeviceLoader = new DataLoader(deviceIds =>
db.threats.findByDeviceIds(deviceIds).then(rows =>
deviceIds.map(id => rows.filter(r => r.deviceId === id))
)
);
// 3. Computed field loader â aggregated data
const threatCountLoader = new DataLoader(deviceIds =>
db.threats.countByDeviceIds(deviceIds).then(counts =>
deviceIds.map(id => counts[id] || 0)
)
);
// 4. Cross-reference loader
const devicesByUserLoader = new DataLoader(userIds =>
db.devices.findByUserIds(userIds).then(rows =>
userIds.map(id => rows.filter(r => r.userId === id))
)
);
Query Performance Monitoring
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [{
requestDidStart({ request }) {
const startTime = process.hrtime.bigint();
const query = request.query?.substring(0, 200);
return {
willSendResponse({ context, errors }) {
const duration = Number(process.hrtime.bigint() - startTime) / 1e6;
// Log slow queries
if (duration > 500) {
logger.warn('Slow query', {
operation: request.operationName,
duration: `${duration.toFixed(0)}ms`,
query,
userId: context.user?.id,
});
}
// Track metrics
metrics.timing('graphql.query.duration', duration, {
operation: request.operationName || 'anonymous',
hasErrors: (errors?.length || 0) > 0,
});
},
};
},
}],
});
N+1 Query Detection
// Log DataLoader batch efficiency
function createMonitoredLoader(name, batchFn, options = {}) {
const loader = new DataLoader(async (keys) => {
const startTime = Date.now();
const results = await batchFn(keys);
const duration = Date.now() - startTime;
logger.debug(`DataLoader ${name}: ${keys.length} keys in ${duration}ms`);
// Alert on single-key loads (possible N+1)
if (keys.length === 1 && keys.length < 100) {
logger.warn(`DataLoader ${name}: single-key load detected`);
}
return results;
}, options);
return loader;
}
// Usage
const context = {
threatLoader: createMonitoredLoader('threats', batchThreats),
userLoader: createMonitoredLoader('users', batchUsers),
};
Query Batching Strategies
// Merge multiple small queries into one
// Before: 10 separate requests
// After: 1 request with aliases
query DashboardData {
criticalThreats: threats(severity: CRITICAL, limit: 5) {
id name severity detectedAt
}
onlineDevices: devices(status: ONLINE) {
id name os lastScan
}
userAlerts: myAlerts(unread: true) {
id message createdAt
}
systemHealth: health {
status uptime activeScans
}
}
Common Mistakes
1. No Response Caching for Public Data
Threat lists, device catalogs, and severity enums rarely change. Without caching, every request hits resolvers and databases.
2. Not Using DataLoader for All Relationships
Every findByXId call in a resolver without DataLoader creates an N+1 problem. Use DataLoader for ALL relationship resolvers.
3. Ignoring Client-Side Caching
Apollo Client's InMemoryCache normalizes data by ID. Ensure every type has a unique id: ID! field for proper cache normalization.
4. Not Monitoring Query Performance
Without monitoring, you don't know which queries are slow. Log duration per operation and set up alerts for queries exceeding 500ms.
5. Overfetching in Parent Resolvers
Loading all related data in the root resolver defeats GraphQL's selection optimization. Let each level fetch its own data via DataLoader.
Practice Questions
- What is the most impactful GraphQL performance optimization?
- How do you cache GraphQL responses on the server?
- How do you optimize nested resolver queries?
- What is the role of CDN in GraphQL performance?
- How do you detect N+1 queries in production?
Answers:
- DataLoader batching â it reduces N+1 queries to 2 queries regardless of data size. Without it, performance degrades linearly with data volume.
- Use
responseCachePluginwith@cacheControldirectives on types and fields. Cache public data (minutes) and skip caching for user-specific data. - Use DataLoader for ALL relationship resolvers. Batch loads within each request. Create loaders per request in the context Factory.
- CDN caches GraphQL GET requests (persisted queries) at edge locations. Subsequent requests from nearby users are served from the edge cache, not the origin server.
- Log DataLoader batch sizes â if a loader frequently receives single keys, it indicates N+1. Also log resolver execution counts for relationship fields.
Challenge: Optimize DodaTech's GraphQL API for performance. Implement response caching with appropriate TTLs (public data: 5min, device data: 30s, user data: 0). Set up DataLoader for all entity relationships with monitoring. Configure persisted queries for CDN caching. Implement query performance monitoring with slow-query alerts. Target: dashboard loads in under 200ms.
FAQ
Mini Project
Optimize DodaTech's GraphQL API from first principles. Measure baseline performance, implement DataLoader for all relationships, add response caching with type-specific TTLs, configure persisted queries with CDN integration, implement query monitoring with Datadog/Prometheus, and benchmark the before/after performance improvement.
What's Next
| Topic | Description |
|---|---|
| Full-Stack Project | Build a complete GraphQL application |
| Security | Protecting your GraphQL API |
| Testing | Testing GraphQL APIs |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro