Skip to content

docs: add guide for operation complexity controls #4402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cspell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ overrides:
- URQL
- tada
- Graphile
- precompiled
- debuggable

ignoreRegExpList:
- u\{[0-9a-f]{1,8}\}
Expand Down Expand Up @@ -130,4 +132,3 @@ words:
- overcomplicating
- cacheable
- pino
- debuggable
1 change: 1 addition & 0 deletions website/pages/docs/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const meta = {
'cursor-based-pagination': '',
'custom-scalars': '',
'advanced-custom-scalars': '',
'operation-complexity-controls': '',
'n1-dataloader': '',
'caching-strategies': '',
'resolver-anatomy': '',
Expand Down
264 changes: 264 additions & 0 deletions website/pages/docs/operation-complexity-controls.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
---
title: Operation Complexity Controls
---

import { Callout } from 'nextra/components'

# Operation Complexity Controls

GraphQL gives clients a lot of flexibility to shape responses, but that
flexibility can also introduce risk. Clients can request deeply nested fields or
large volumes of data in a single operation. Without controls, these operations can slow
down your server or expose security vulnerabilities.

This guide explains how to measure and limit operation complexity in GraphQL.js
using static analysis. You'll learn how to estimate the cost
of an operation before execution and reject it if it exceeds a safe limit.

<Callout type="info" emoji="ℹ️">
In production, we recommend using [trusted documents](/docs/going-to-production#only-allow-trusted-documents)
rather than analyzing arbitrary documents at runtime. Complexity analysis can still be
useful at build time to catch expensive operations before they're deployed.
</Callout>

## Why complexity control matters

GraphQL lets clients choose exactly what data they want. That flexibility is powerful,
but it also makes it hard to predict the runtime cost of a query just by looking
at the schema.

Without safeguards, clients could:

- Request deeply nested object relationships
- Use nested fragments to multiply field resolution
- Exploit pagination arguments to retrieve excessive data

Certain field types (e.g., lists, interfaces, unions) can also significantly
increase cost by multiplying the number of values returned or resolved.

Complexity controls help prevent these issues. They allow you to:

- Protect your backend from denial-of-service attacks or accidental load
- Enforce cost-based usage limits between clients or environments
- Detect expensive queries early in development
- Add an additional layer of protection alongside authentication, depth limits, and validation

For more information, see [Security best practices](https://graphql.org/learn/security/).

## Estimating operation cost

To measure a query's complexity, you typically:

1. Parse the incoming operation into a GraphQL document.
2. Walk the query's Abstract Syntax Tree (AST), which represents its structure.
3. Assign a cost to each field, often using static heuristics or metadata.
4. Reject or log the operation if it exceeds a maximum allowed complexity.

You can do this using custom middleware or validation rules that run before execution.
No resolvers are called unless the operation passes these checks.

<Callout type="info" emoji="ℹ️">
Fragment cycles or deep nesting can cause some complexity analyzers to perform
poorly or get stuck. Always run complexity analysis after validation unless your analyzer
explicitly handles cycles safely.
</Callout>

## Simple complexity calculation

There are several community-maintained tools for complexity analysis. The examples in this
guide use [`graphql-query-complexity`](https://github.com/slicknode/graphql-query-complexity) as
an option, but we recommend choosing the approach that best fits your project.

Here's a basic example using its `simpleEstimator`, which assigns a flat cost to every field:

```js
import { parse } from 'graphql';
import { getComplexity, simpleEstimator } from 'graphql-query-complexity';
import { schema } from './schema.js';

const query = `
query {
users {
id
name
posts {
id
title
}
}
}
`;

const complexity = getComplexity({
schema,
query: parse(query),
estimators: [simpleEstimator({ defaultComplexity: 1 })],
variables: {},
});

if (complexity > 100) {
throw new Error(`Query is too complex: ${complexity}`);
}

console.log(`Query complexity: ${complexity}`);
```

In this example, every field costs 1 point. The total complexity is the number of fields,
adjusted for nesting and fragments. The complexity is calculated before execution begins,
allowing you to reject costly operations early.

## Custom cost estimators

Some fields are more expensive than others. For example, a paginated list might be more
costly than a scalar field. You can define per-field costs using
`fieldExtensionsEstimator`, a feature supported by some complexity tools.

This estimator reads cost metadata from the field's `extensions.complexity` function in
your schema. For example:

```js
import { GraphQLObjectType, GraphQLList, GraphQLInt } from 'graphql';
import { PostType } from './post-type.js';

const UserType = new GraphQLObjectType({
name: 'User',
fields: {
posts: {
type: GraphQLList(PostType),
args: {
limit: { type: GraphQLInt },
},
extensions: {
complexity: ({ args, childComplexity }) => {
const limit = args.limit ?? 10;
return childComplexity * limit;
},
},
},
},
});
```

In this example, the cost of `posts` depends on the number of items requested (`limit`) and the
complexity of each child field.

<Callout type="info" emoji="ℹ️">
Most validation steps don't have access to variable values. If your complexity
calculation depends on variables (like `limit`), make sure it runs after validation, not
as part of it.
</Callout>

To evaluate the cost before execution, you can combine estimators like this:

```js
import { parse } from 'graphql';
import {
getComplexity,
simpleEstimator,
fieldExtensionsEstimator,
} from 'graphql-query-complexity';
import { schema } from './schema.js';

const query = `
query {
users {
id
posts(limit: 5) {
id
title
}
}
}
`;

const document = parse(query);

const complexity = getComplexity({
schema,
query: document,
variables: {},
estimators: [
fieldExtensionsEstimator(),
simpleEstimator({ defaultComplexity: 1 }),
],
});

console.log(`Query complexity: ${complexity}`);
```

Estimators are evaluated in order. The first one to return a numeric value is used
for a given field. This lets you define detailed logic for specific fields and fall back
to a default cost elsewhere.

## Enforcing limits in your server

Some tools allow you to enforce complexity limits during validation by adding a rule
to your GraphQL.js server. For example, `graphql-query-complexity` provides a
`createComplexityRule` helper:

```js
import { graphql, specifiedRules, parse } from 'graphql';
import { createComplexityRule, simpleEstimator } from 'graphql-query-complexity';
import { schema } from './schema.js';

const source = `
query {
users {
id
posts {
title
}
}
}
`;

const document = parse(source);

const result = await graphql({
schema,
source,
validationRules: [
...specifiedRules,
createComplexityRule({
estimators: [simpleEstimator({ defaultComplexity: 1 })],
maximumComplexity: 50,
onComplete: (complexity) => {
console.log('Query complexity:', complexity);
},
}),
],
});

console.log(result);
```

<Callout type="info" emoji="ℹ️">
Only use complexity rules in validation if you're sure the analysis is cycle-safe.
Otherwise, run complexity checks after validation and before execution.
</Callout>

## Complexity in trusted environments

In environments that use persisted or precompiled operations, complexity analysis is still
useful, just in a different way. You can run it at build time to:

- Warn engineers about expensive operations during development
- Track changes to operation cost across schema changes
- Define internal usage budgets by team, client, or role

## Best practices

- Only accept trusted documents in production when possible.
- Use complexity analysis as a development-time safeguard.
- Avoid running untrusted operations without additional validation and cost checks.
- Account for list fields and abstract types, which can significantly increase cost.
- Avoid estimating complexity before validation unless you're confident in your tooling.
- Use complexity analysis as part of your layered security strategy, alongside depth limits,
field guards, and authentication.

## Additional resources

- [`graphql-query-complexity`](https://github.com/slicknode/graphql-query-complexity): A community-maintained static analysis tool
- [`graphql-depth-limit`](https://github.com/graphile/depth-limit): A lightweight tool to restrict the maximum query depth
- [GraphQL Specification: Operations and execution](https://spec.graphql.org/draft/#sec-Language.Operations)
- [GraphQL.org: Security best practices](https://graphql.org/learn/security/)
Loading