diff --git a/cspell.yml b/cspell.yml
index f324e1fc08..e2d0c9b754 100644
--- a/cspell.yml
+++ b/cspell.yml
@@ -35,6 +35,8 @@ overrides:
- URQL
- tada
- Graphile
+ - precompiled
+ - debuggable
ignoreRegExpList:
- u\{[0-9a-f]{1,8}\}
@@ -130,4 +132,3 @@ words:
- overcomplicating
- cacheable
- pino
- - debuggable
diff --git a/website/pages/docs/_meta.ts b/website/pages/docs/_meta.ts
index 0cb869e0c7..5839672efa 100644
--- a/website/pages/docs/_meta.ts
+++ b/website/pages/docs/_meta.ts
@@ -26,6 +26,7 @@ const meta = {
'cursor-based-pagination': '',
'custom-scalars': '',
'advanced-custom-scalars': '',
+ 'operation-complexity-controls': '',
'n1-dataloader': '',
'caching-strategies': '',
'resolver-anatomy': '',
diff --git a/website/pages/docs/operation-complexity-controls.mdx b/website/pages/docs/operation-complexity-controls.mdx
new file mode 100644
index 0000000000..9aff4dc67e
--- /dev/null
+++ b/website/pages/docs/operation-complexity-controls.mdx
@@ -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.
+
+
+ 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.
+
+
+## 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.
+
+
+ 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.
+
+
+## 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.
+
+
+ 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.
+
+
+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);
+```
+
+
+ Only use complexity rules in validation if you're sure the analysis is cycle-safe.
+ Otherwise, run complexity checks after validation and before execution.
+
+
+## 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/)