Skip to content

Commit 17f16de

Browse files
sarahxsandersbenjie
authored andcommitted
docs: add guide for operation complexity controls (#4402)
Adds guide "Operation Complexity Controls" --------- Co-authored-by: Benjie <[email protected]>
1 parent 737a74c commit 17f16de

File tree

3 files changed

+267
-1
lines changed

3 files changed

+267
-1
lines changed

cspell.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ overrides:
3939
- URQL
4040
- tada
4141
- Graphile
42+
- precompiled
43+
- debuggable
4244

4345
validateDirectives: true
4446
ignoreRegExpList:
@@ -186,4 +188,3 @@ words:
186188
- overcomplicating
187189
- cacheable
188190
- pino
189-
- debuggable

website/pages/docs/_meta.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const meta = {
2727
'cursor-based-pagination': '',
2828
'custom-scalars': '',
2929
'advanced-custom-scalars': '',
30+
'operation-complexity-controls': '',
3031
'n1-dataloader': '',
3132
'caching-strategies': '',
3233
'resolver-anatomy': '',
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
---
2+
title: Operation Complexity Controls
3+
---
4+
5+
import { Callout } from 'nextra/components'
6+
7+
# Operation Complexity Controls
8+
9+
GraphQL gives clients a lot of flexibility to shape responses, but that
10+
flexibility can also introduce risk. Clients can request deeply nested fields or
11+
large volumes of data in a single operation. Without controls, these operations can slow
12+
down your server or expose security vulnerabilities.
13+
14+
This guide explains how to measure and limit operation complexity in GraphQL.js
15+
using static analysis. You'll learn how to estimate the cost
16+
of an operation before execution and reject it if it exceeds a safe limit.
17+
18+
<Callout type="info" emoji="ℹ️">
19+
In production, we recommend using [trusted documents](/docs/going-to-production#only-allow-trusted-documents)
20+
rather than analyzing arbitrary documents at runtime. Complexity analysis can still be
21+
useful at build time to catch expensive operations before they're deployed.
22+
</Callout>
23+
24+
## Why complexity control matters
25+
26+
GraphQL lets clients choose exactly what data they want. That flexibility is powerful,
27+
but it also makes it hard to predict the runtime cost of a query just by looking
28+
at the schema.
29+
30+
Without safeguards, clients could:
31+
32+
- Request deeply nested object relationships
33+
- Use nested fragments to multiply field resolution
34+
- Exploit pagination arguments to retrieve excessive data
35+
36+
Certain field types (e.g., lists, interfaces, unions) can also significantly
37+
increase cost by multiplying the number of values returned or resolved.
38+
39+
Complexity controls help prevent these issues. They allow you to:
40+
41+
- Protect your backend from denial-of-service attacks or accidental load
42+
- Enforce cost-based usage limits between clients or environments
43+
- Detect expensive queries early in development
44+
- Add an additional layer of protection alongside authentication, depth limits, and validation
45+
46+
For more information, see [Security best practices](https://graphql.org/learn/security/).
47+
48+
## Estimating operation cost
49+
50+
To measure a query's complexity, you typically:
51+
52+
1. Parse the incoming operation into a GraphQL document.
53+
2. Walk the query's Abstract Syntax Tree (AST), which represents its structure.
54+
3. Assign a cost to each field, often using static heuristics or metadata.
55+
4. Reject or log the operation if it exceeds a maximum allowed complexity.
56+
57+
You can do this using custom middleware or validation rules that run before execution.
58+
No resolvers are called unless the operation passes these checks.
59+
60+
<Callout type="info" emoji="ℹ️">
61+
Fragment cycles or deep nesting can cause some complexity analyzers to perform
62+
poorly or get stuck. Always run complexity analysis after validation unless your analyzer
63+
explicitly handles cycles safely.
64+
</Callout>
65+
66+
## Simple complexity calculation
67+
68+
There are several community-maintained tools for complexity analysis. The examples in this
69+
guide use [`graphql-query-complexity`](https://github.com/slicknode/graphql-query-complexity) as
70+
an option, but we recommend choosing the approach that best fits your project.
71+
72+
Here's a basic example using its `simpleEstimator`, which assigns a flat cost to every field:
73+
74+
```js
75+
import { parse } from 'graphql';
76+
import { getComplexity, simpleEstimator } from 'graphql-query-complexity';
77+
import { schema } from './schema.js';
78+
79+
const query = `
80+
query {
81+
users {
82+
id
83+
name
84+
posts {
85+
id
86+
title
87+
}
88+
}
89+
}
90+
`;
91+
92+
const complexity = getComplexity({
93+
schema,
94+
query: parse(query),
95+
estimators: [simpleEstimator({ defaultComplexity: 1 })],
96+
variables: {},
97+
});
98+
99+
if (complexity > 100) {
100+
throw new Error(`Query is too complex: ${complexity}`);
101+
}
102+
103+
console.log(`Query complexity: ${complexity}`);
104+
```
105+
106+
In this example, every field costs 1 point. The total complexity is the number of fields,
107+
adjusted for nesting and fragments. The complexity is calculated before execution begins,
108+
allowing you to reject costly operations early.
109+
110+
## Custom cost estimators
111+
112+
Some fields are more expensive than others. For example, a paginated list might be more
113+
costly than a scalar field. You can define per-field costs using
114+
`fieldExtensionsEstimator`, a feature supported by some complexity tools.
115+
116+
This estimator reads cost metadata from the field's `extensions.complexity` function in
117+
your schema. For example:
118+
119+
```js
120+
import { GraphQLObjectType, GraphQLList, GraphQLInt } from 'graphql';
121+
import { PostType } from './post-type.js';
122+
123+
const UserType = new GraphQLObjectType({
124+
name: 'User',
125+
fields: {
126+
posts: {
127+
type: GraphQLList(PostType),
128+
args: {
129+
limit: { type: GraphQLInt },
130+
},
131+
extensions: {
132+
complexity: ({ args, childComplexity }) => {
133+
const limit = args.limit ?? 10;
134+
return childComplexity * limit;
135+
},
136+
},
137+
},
138+
},
139+
});
140+
```
141+
142+
In this example, the cost of `posts` depends on the number of items requested (`limit`) and the
143+
complexity of each child field.
144+
145+
<Callout type="info" emoji="ℹ️">
146+
Most validation steps don't have access to variable values. If your complexity
147+
calculation depends on variables (like `limit`), make sure it runs after validation, not
148+
as part of it.
149+
</Callout>
150+
151+
To evaluate the cost before execution, you can combine estimators like this:
152+
153+
```js
154+
import { parse } from 'graphql';
155+
import {
156+
getComplexity,
157+
simpleEstimator,
158+
fieldExtensionsEstimator,
159+
} from 'graphql-query-complexity';
160+
import { schema } from './schema.js';
161+
162+
const query = `
163+
query {
164+
users {
165+
id
166+
posts(limit: 5) {
167+
id
168+
title
169+
}
170+
}
171+
}
172+
`;
173+
174+
const document = parse(query);
175+
176+
const complexity = getComplexity({
177+
schema,
178+
query: document,
179+
variables: {},
180+
estimators: [
181+
fieldExtensionsEstimator(),
182+
simpleEstimator({ defaultComplexity: 1 }),
183+
],
184+
});
185+
186+
console.log(`Query complexity: ${complexity}`);
187+
```
188+
189+
Estimators are evaluated in order. The first one to return a numeric value is used
190+
for a given field. This lets you define detailed logic for specific fields and fall back
191+
to a default cost elsewhere.
192+
193+
## Enforcing limits in your server
194+
195+
Some tools allow you to enforce complexity limits during validation by adding a rule
196+
to your GraphQL.js server. For example, `graphql-query-complexity` provides a
197+
`createComplexityRule` helper:
198+
199+
```js
200+
import { graphql, specifiedRules, parse } from 'graphql';
201+
import { createComplexityRule, simpleEstimator } from 'graphql-query-complexity';
202+
import { schema } from './schema.js';
203+
204+
const source = `
205+
query {
206+
users {
207+
id
208+
posts {
209+
title
210+
}
211+
}
212+
}
213+
`;
214+
215+
const document = parse(source);
216+
217+
const result = await graphql({
218+
schema,
219+
source,
220+
validationRules: [
221+
...specifiedRules,
222+
createComplexityRule({
223+
estimators: [simpleEstimator({ defaultComplexity: 1 })],
224+
maximumComplexity: 50,
225+
onComplete: (complexity) => {
226+
console.log('Query complexity:', complexity);
227+
},
228+
}),
229+
],
230+
});
231+
232+
console.log(result);
233+
```
234+
235+
<Callout type="info" emoji="ℹ️">
236+
Only use complexity rules in validation if you're sure the analysis is cycle-safe.
237+
Otherwise, run complexity checks after validation and before execution.
238+
</Callout>
239+
240+
## Complexity in trusted environments
241+
242+
In environments that use persisted or precompiled operations, complexity analysis is still
243+
useful, just in a different way. You can run it at build time to:
244+
245+
- Warn engineers about expensive operations during development
246+
- Track changes to operation cost across schema changes
247+
- Define internal usage budgets by team, client, or role
248+
249+
## Best practices
250+
251+
- Only accept trusted documents in production when possible.
252+
- Use complexity analysis as a development-time safeguard.
253+
- Avoid running untrusted operations without additional validation and cost checks.
254+
- Account for list fields and abstract types, which can significantly increase cost.
255+
- Avoid estimating complexity before validation unless you're confident in your tooling.
256+
- Use complexity analysis as part of your layered security strategy, alongside depth limits,
257+
field guards, and authentication.
258+
259+
## Additional resources
260+
261+
- [`graphql-query-complexity`](https://github.com/slicknode/graphql-query-complexity): A community-maintained static analysis tool
262+
- [`graphql-depth-limit`](https://github.com/graphile/depth-limit): A lightweight tool to restrict the maximum query depth
263+
- [GraphQL Specification: Operations and execution](https://spec.graphql.org/draft/#sec-Language.Operations)
264+
- [GraphQL.org: Security best practices](https://graphql.org/learn/security/)

0 commit comments

Comments
 (0)