| name | graphql-performance |
| user-invocable | false |
| description | Use when optimizing GraphQL API performance with query complexity analysis, batching, caching strategies, depth limiting, monitoring, and database optimization. |
| allowed-tools | [] |
GraphQL Performance
Apply GraphQL performance optimization techniques to create efficient,
scalable APIs. This skill covers query complexity analysis, depth
limiting, batching and caching strategies, DataLoader optimization,
monitoring, tracing, and database query optimization.
Query Complexity Analysis
Query complexity analysis prevents expensive queries from overwhelming
your server by calculating and limiting the computational cost.
import { GraphQLError } from 'graphql';
import { ApolloServer } from '@apollo/server';
const getComplexity = (field, childComplexity, args) => {
let complexity = 1;
if (args.limit) {
complexity = args.limit;
} else if (args.first) {
complexity = args.first;
}
return complexity + childComplexity;
};
const schema = `
directive @complexity(
value: Int!
multipliers: [String!]
) on FIELD_DEFINITION
type Query {
user(id: ID!): User @complexity(value: 1)
users(limit: Int): [User!]! @complexity(
value: 1,
multipliers: ["limit"]
)
posts(first: Int): [Post!]! @complexity(
value: 5,
multipliers: ["first"]
)
}
type User {
id: ID!
posts: [Post!]! @complexity(value: 10)
}
`;
const complexityPlugin = {
requestDidStart: () => ({
async didResolveOperation({ request, document, operationName }) {
const complexity = calculateComplexity({
document,
operationName,
variables: request.variables
});
const maxComplexity = 1000;
if (complexity > maxComplexity) {
throw new GraphQLError(
`Query is too complex: ${complexity}. ` +
`Maximum allowed: ${maxComplexity}`,
{
extensions: {
code: 'QUERY_TOO_COMPLEX',
complexity,
maxComplexity
}
}
);
}
}
})
};
const calculateComplexity = ({ document, operationName, variables }) => {
let totalComplexity = 0;
const visit = (node, multiplier = 1) => {
if (node.kind === 'Field') {
const complexity = getFieldComplexity(node);
const args = getArguments(node, variables);
const fieldMultiplier = getMultiplier(args);
totalComplexity += complexity * multiplier * fieldMultiplier;
if (node.selectionSet) {
node.selectionSet.selections.forEach(child =>
visit(child, multiplier * fieldMultiplier)
);
}
}
};
visit(document);
return totalComplexity;
};
Depth Limiting
Prevent deeply nested queries that can cause performance issues and
potential denial of service attacks.
import { ValidationContext, GraphQLError } from 'graphql';
const depthLimit = (maxDepth: number) => {
return (validationContext: ValidationContext) => {
return {
Field(node, key, parent, path, ancestors) {
const depth = ancestors.filter(
ancestor => ancestor.kind === 'Field'
).length;
if (depth > maxDepth) {
validationContext.reportError(
new GraphQLError(
`Query exceeds maximum depth of ${maxDepth}. ` +
`Found depth of ${depth}.`,
{
nodes: [node],
extensions: {
code: 'DEPTH_LIMIT_EXCEEDED',
depth,
maxDepth
}
}
)
);
}
}
};
};
};
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(7)]
});
query {
user {
posts {
comments {
author {
username
}
}
}
}
}
query {
user {
friends {
friends {
friends {
friends {
friends {
friends {
friends {
username
}
}
}
}
}
}
}
}
}
Query Cost Analysis
Implement cost-based rate limiting to protect against expensive
queries.
interface CostConfig {
objectCost: number;
scalarCost: number;
defaultListSize: number;
}
const calculateQueryCost = (
document,
variables,
config: CostConfig
) => {
let totalCost = 0;
const visit = (node, multiplier = 1) => {
if (node.kind === 'Field') {
const fieldType = getFieldType(node);
if (isListType(fieldType)) {
const listSize = getListSize(node, variables) ||
config.defaultListSize;
multiplier *= listSize;
}
if (isObjectType(fieldType)) {
totalCost += config.objectCost * multiplier;
} else {
totalCost += config.scalarCost * multiplier;
}
if (node.selectionSet) {
node.selectionSet.selections.forEach(child =>
visit(child, multiplier)
);
}
}
};
();
totalCost;
};
costLimitPlugin = {
: ({
() {
cost = (
,
request.,
{ : , : , : }
);
limit = (contextValue.);
used = (contextValue.);
(used + cost > limit) {
(, {
: {
: ,
cost,
used,
limit
}
});
}
(contextValue., cost);
}
})
};
Batching with DataLoader
Optimize data fetching by batching multiple requests into single
database queries.
import DataLoader from 'dataloader';
const createUserLoader = (db) => {
return new DataLoader<string, User>(
async (userIds) => {
const users = await db.users.findByIds(userIds);
const userMap = new Map(users.map(u => [u.id, u]));
return userIds.map(id => userMap.get(id) || null);
},
{
cache: true,
maxBatchSize: 100,
batchScheduleFn: callback => setTimeout(callback, 10)
}
);
};
const createPostsLoader = (db) => {
return <, []>(
(authorIds) => {
posts = db..()
.(, authorIds)
.();
postsByAuthor = authorIds.(
posts.( post. === authorId)
);
postsByAuthor;
}
);
};
{
: ;
: ;
}
= () => {
<, []>(
(keys) => {
authorIds = [... (keys.( k.))];
statuses = [... (keys.( k.))];
posts = db..()
.(, authorIds)
.(, statuses)
.();
keys.(
posts.(
post. === key. &&
post. === key.
)
);
},
{
:
}
);
};
{ } ;
= () => {
cache = <, >({
: ,
: * *
});
<, >(
(userIds) => {
users = db..(userIds);
userMap = (users.( [u., u]));
userIds.( userMap.(id) || );
},
{
: cache
}
);
};
Response Caching Strategies
Implement multi-level caching for optimal performance.
import { createHash } from 'crypto';
const cacheControl = {
User: {
__cacheControl: { maxAge: 3600 },
posts: {
__cacheControl: { maxAge: 300 }
}
},
Post: {
__cacheControl: { maxAge: 600, scope: 'PUBLIC' }
}
};
import Redis from 'ioredis';
const redis = new Redis();
const cacheQuery = async (key: string, ttl: number, fn: () => any) => {
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
const result = await fn();
await redis.setex(key, ttl, .(result));
result;
};
resolvers = {
: {
: (_, args) => {
cacheKey = ;
(cacheKey, , () => {
db..(args);
});
},
: (_, { id }) => {
cacheKey = ;
(cacheKey, , () => {
db..(id);
});
}
}
};
server = ({
typeDefs,
resolvers,
: [
{
() {
{
() {
(!request.?.?.) {
;
}
{
: {
: ([
[, ]
])
}
};
}
};
}
}
]
});
Persistent Queries and APQ
Implement Automatic Persisted Queries to reduce payload size and
enable better caching.
import { ApolloServer } from '@apollo/server';
import { KeyvAdapter } from '@apollo/utils.keyvadapter';
import Keyv from 'keyv';
const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries: {
cache: new KeyvAdapter(new Keyv('redis://localhost:6379'))
}
});
const allowedQueries = new Map([
['getUser', 'query GetUser($id: ID!) { user(id: $id) { id name } }'],
['getPosts', 'query GetPosts { posts { id title } }']
]);
const whitelistPlugin = {
: ({
() {
hash = source.?.?.;
(!hash || !allowedQueries.(hash)) {
(, {
: { : }
});
}
}
})
};
Database Query Optimization
Optimize database queries to support GraphQL efficiently.
import { GraphQLResolveInfo } from 'graphql';
import { parseResolveInfo } from 'graphql-parse-resolve-info';
const resolvers = {
Query: {
users: async (_, args, { db }, info: GraphQLResolveInfo) => {
const parsedInfo = parseResolveInfo(info);
const fields = Object.keys(parsedInfo.fields);
return db.users.query().select(fields);
},
posts: async (_, args, { db }, info: GraphQLResolveInfo) => {
const parsedInfo = parseResolveInfo(info);
let query = db.posts.query();
if (parsedInfo.fields.author) {
query = query.withGraphFetched('author');
}
if (parsedInfo.fields.comments) {
query = query.();
}
query;
}
}
};
optimizedResolvers = {
: {
: (_, { limit, offset }, { db }) => {
db..()
.(limit)
.(offset)
.();
}
},
: {
: (parent, _, { db }) => {
(parent.) {
parent.;
}
db..().(, parent.);
}
}
};
Monitoring and Profiling
Implement comprehensive monitoring to identify performance bottlenecks.
import { ApolloServer } from '@apollo/server';
const timingPlugin = {
requestDidStart() {
const start = Date.now();
return {
async willSendResponse({ response }) {
const duration = Date.now() - start;
response.extensions = {
...response.extensions,
timing: { duration }
};
}
};
}
};
const detailedTimingPlugin = {
requestDidStart() {
const resolverTimings = {};
return {
async executionDidStart() {
return {
willResolveField({ info }) {
const start = Date.now();
return () => {
const duration = Date.now() - start;
const path = info.path.key;
resolverTimings[path] = duration;
};
}
};
},
async () {
response. = {
...response.,
resolverTimings
};
}
};
}
};
performancePlugin = {
() {
{
() {
(, , {
: operation.,
: operation.?. ||
});
},
() {
errors.( {
(, , {
: error.?. ||
});
});
},
() {
responseSize = .(response).;
(, responseSize);
}
};
}
};
Tracing and Observability
Implement distributed tracing for GraphQL operations.
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracingPlugin = {
requestDidStart() {
const tracer = trace.getTracer('graphql-server');
return {
async didResolveOperation({ request, operation }) {
const span = tracer.startSpan('graphql.operation', {
attributes: {
'graphql.operation.type': operation.operation,
'graphql.operation.name': operation.name?.value
}
});
return {
async executionDidStart() {
return {
willResolveField({ info }) {
const fieldSpan = tracer.startSpan(
`graphql.resolve.${info.fieldName}`,
{ attributes: { 'graphql.field': info.fieldName } }
);
return () => {
fieldSpan.end();
};
}
};
},
async willSendResponse({ errors }) {
(errors) {
span.({
: .,
: errors[].
});
}
span.();
}
};
}
};
}
};
server = ({
typeDefs,
resolvers,
: [
()(),
{
() {
{
() {
(process..) {
({
: metrics.,
: metrics.,
: response.
});
}
}
};
}
}
]
});
Pagination Optimization
Implement efficient pagination strategies for large datasets.
const resolvers = {
Query: {
posts: async (_, { first, after }, { db }) => {
const limit = first || 10;
let query = db.posts.query()
.orderBy('createdAt', 'desc')
.limit(limit + 1);
if (after) {
const cursor = decodeCursor(after);
query = query.where('createdAt', '<', cursor.createdAt);
}
const posts = await query;
const hasNextPage = posts.length > limit;
const edges = posts.slice(0, limit).map(post => ({
cursor: encodeCursor({ createdAt: post.createdAt }),
node: post
}));
return {
edges,
pageInfo: {
hasNextPage,
endCursor: edges[edges.length - 1]?.cursor
}
};
}
}
};
const = () => {
query = (table)
.([
{ : , : },
{ : , : }
])
.(limit + );
(after) {
cursor = .(.(after, ).());
query = query.(() {
.(, , cursor.)
.(() {
.(, , cursor.)
.(, , cursor.);
});
});
}
query;
};
Best Practices
- Implement query complexity limits: Prevent expensive queries
from overwhelming your server with complexity analysis
- Use depth limiting: Set maximum query depth to prevent deeply
nested queries that cause performance issues
- Batch with DataLoader: Always use DataLoader for related data to
avoid N+1 query problems
- Cache strategically: Implement multi-level caching (DataLoader,
Redis, CDN) based on data volatility
- Monitor performance: Track resolver timing, query complexity,
and error rates to identify bottlenecks
- Optimize database queries: Use selective field loading and
conditional joins based on requested fields
- Implement APQ: Use Automatic Persisted Queries to reduce payload
size and enable CDN caching
- Use cursor pagination: Prefer cursor-based pagination over
offset for large datasets
- Add proper indexes: Create database indexes for common query
patterns and filter fields
- Enable tracing: Use OpenTelemetry or Apollo Studio for
distributed tracing and debugging
Common Pitfalls
- No query limits: Allowing unbounded queries that can cause
denial of service
- Inefficient resolvers: Writing resolvers that don't use batching
or caching, causing N+1 problems
- Missing indexes: Not creating database indexes for GraphQL query
patterns
- Over-caching: Caching data too aggressively, leading to stale
data being served
- Ignoring info parameter: Not using GraphQLResolveInfo to
optimize field selection
- No monitoring: Deploying without performance monitoring and
unable to identify issues
- Blocking operations: Using synchronous operations in resolvers
that block the event loop
- Inefficient pagination: Using offset-based pagination for large
datasets
- No rate limiting: Allowing unlimited queries per user without
cost-based limits
- Cache stampede: Not handling cache expiration properly, causing
all requests to hit the database simultaneously
When to Use This Skill
Use GraphQL performance optimization skills when:
- Building a new GraphQL API that needs to scale
- Experiencing slow query response times
- Debugging N+1 query problems in production
- Implementing rate limiting and query cost analysis
- Adding caching layers to improve performance
- Optimizing database queries for GraphQL patterns
- Setting up monitoring and observability
- Protecting against malicious or expensive queries
- Migrating to production and need performance tuning
- Identifying and fixing performance bottlenecks
Resources