Rate Limiting and Depth Limiting for GraphQL API
GraphQL allows clients to form arbitrarily complex queries. Without restrictions, single query can request millions of records through nested relations or create CPU-killer query with nesting depth of 50 levels. GraphQL rate limiting differs from REST: cannot just count requests — must account for complexity.
Depth Limiting
Limit maximum AST tree nesting depth:
import depthLimit from 'graphql-depth-limit'
import { ApolloServer } from '@apollo/server'
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(7) // maximum 7 levels nesting
]
})
Attack without depth limit:
# This query is legal but recursively creates millions of objects
{
user {
friends {
friends {
friends {
friends {
friends { id name }
}
}
}
}
}
}
Query Complexity
Depth doesn't account for query width. graphql-query-complexity counts total cost:
import { createComplexityLimitRule } from 'graphql-query-complexity'
import { fieldExtensionsEstimator, simpleEstimator } from 'graphql-query-complexity'
const complexityRule = createComplexityLimitRule(1000, {
estimators: [
// Take complexity from SDL @complexity directive
fieldExtensionsEstimator(),
// Account for pagination arguments
({
type, field, args, childComplexity
}) => {
if (args.limit) {
return args.limit * childComplexity
}
if (args.first) {
return args.first * childComplexity
}
return 1 + childComplexity
},
// Base field cost = 1
simpleEstimator({ defaultComplexity: 1 })
],
onSuccess: (complexity) => {
console.log(`Query complexity: ${complexity}`)
},
formatErrorMessage: (complexity) =>
`Query too complex (${complexity}). Max allowed: 1000`
})
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(7),
complexityRule
]
})
Complexity in SDL with Directives
directive @complexity(value: Int!, multipliers: [String!]) on FIELD_DEFINITION
type Query {
# Simple field: complexity 1
user(id: ID!): User
# List: complexity = first * childComplexity
posts(first: Int = 10): [Post!]! @complexity(value: 1, multipliers: ["first"])
# Expensive operation: complexity 10
searchUsers(query: String!): [User!]! @complexity(value: 10)
}
Rate Limiting by Operation
// Different limits for different operation types
class GraphQLRateLimiter {
constructor(redis) {
this.r = redis
}
async checkRequest(userId, operationName, complexity) {
const now = Math.floor(Date.now() / 1000)
const minute = now - (now % 60)
// 1. Limit by number of operations (requests) per minute
const opsKey = `gql:ops:${userId}:${minute}`







