From 479b7195cfda6f50a2d35851dd77aeb26688c1b6 Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Mon, 19 May 2025 21:19:32 -0400 Subject: [PATCH 1/6] add guide for query complexity controls --- website/pages/docs/_meta.ts | 1 + .../pages/docs/query-complexity-controls.mdx | 230 ++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 website/pages/docs/query-complexity-controls.mdx diff --git a/website/pages/docs/_meta.ts b/website/pages/docs/_meta.ts index 9b09249e88..6b3a8c3204 100644 --- a/website/pages/docs/_meta.ts +++ b/website/pages/docs/_meta.ts @@ -23,6 +23,7 @@ const meta = { 'cursor-based-pagination': '', 'custom-scalars': '', 'advanced-custom-scalars': '', + 'query-complexity-controls': '', 'n1-dataloader': '', 'resolver-anatomy': '', 'graphql-errors': '', diff --git a/website/pages/docs/query-complexity-controls.mdx b/website/pages/docs/query-complexity-controls.mdx new file mode 100644 index 0000000000..179fc70b06 --- /dev/null +++ b/website/pages/docs/query-complexity-controls.mdx @@ -0,0 +1,230 @@ +--- +title: Query Complexity Controls +--- + +# Query 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 query. Without controls, these operations can slow +down your server or expose security vulnerabilities. + +This guide explains how to measure and limit query complexity in GraphQL.js +using static analysis. You'll learn how to estimate the cost +of a query before execution and reject it if it exceeds a safe limit. + +## 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 recursive fragments to multiply field resolution +- Exploit pagination arguments to retrieve excessive data + +Query 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 + +## Estimating query cost + +To measure a query's complexity, you typically: + +1. Parse the incoming query 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 query 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 query passes these checks. + +## Simple complexity calculation + +The `graphql-query-complexity` package calculates query cost by walking the AST. Here's a +simple example using `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 queries 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`. + +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. + +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 fallback approach allows you to define detailed logic for specific fields and use +a default cost for everything else. + +## Enforcing limits in your server + +To enforce complexity limits automatically, you can use `createComplexityRule` from +the same package. This integrates with GraphQL.js validation and prevents execution of +overly complex queries. + +Here's how to include it in your server's execution flow: + +```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); +``` + +If the query exceeds the defined complexity limit, GraphQL.js will return a validation +error and skip execution. + +This approach is useful when you want to apply global complexity rules without needing +to modify resolver logic or add separate middleware. + +## Best practices + +- Set conservative complexity limits at first, and adjust them based on observed usage. +- Use field-level estimators to better reflect real backend cost. +- Log query complexity in development and production to identify inefficiencies. +- Apply stricter limits for public or unauthenticated clients. +- Combine complexity limits with depth limits, persisted queries, or operation +whitelisting for stronger control. + +## Additional resources + +- [`graphql-query-complexity`](https://github.com/slicknode/graphql-query-complexity): A static analysis tool for measuring query cost in GraphQL.js servers +- [`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/) From 3355470d461659df974fd00b5cf5fec3b3e04b9d Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Tue, 20 May 2025 21:00:43 -0400 Subject: [PATCH 2/6] feedback --- website/pages/docs/_meta.ts | 2 +- ....mdx => operation-complexity-controls.mdx} | 100 ++++++++++++------ 2 files changed, 67 insertions(+), 35 deletions(-) rename website/pages/docs/{query-complexity-controls.mdx => operation-complexity-controls.mdx} (56%) diff --git a/website/pages/docs/_meta.ts b/website/pages/docs/_meta.ts index 6b3a8c3204..116490cae1 100644 --- a/website/pages/docs/_meta.ts +++ b/website/pages/docs/_meta.ts @@ -23,7 +23,7 @@ const meta = { 'cursor-based-pagination': '', 'custom-scalars': '', 'advanced-custom-scalars': '', - 'query-complexity-controls': '', + 'operation-complexity-controls': '', 'n1-dataloader': '', 'resolver-anatomy': '', 'graphql-errors': '', diff --git a/website/pages/docs/query-complexity-controls.mdx b/website/pages/docs/operation-complexity-controls.mdx similarity index 56% rename from website/pages/docs/query-complexity-controls.mdx rename to website/pages/docs/operation-complexity-controls.mdx index 179fc70b06..b37e672687 100644 --- a/website/pages/docs/query-complexity-controls.mdx +++ b/website/pages/docs/operation-complexity-controls.mdx @@ -1,17 +1,23 @@ --- -title: Query Complexity Controls +title: Operation Complexity Controls --- -# Query Complexity Controls +# 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 query. Without controls, these operations can slow +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 query complexity in GraphQL.js +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 a query before execution and reject it if it exceeds a safe limit. +of an operation before execution and reject it if it exceeds a safe limit. + + + In production, we recommend using [trusted documents](https://graphql.org/learn/persistence/) + 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 @@ -25,28 +31,43 @@ Without safeguards, clients could: - Use recursive fragments to multiply field resolution - Exploit pagination arguments to retrieve excessive data -Query complexity controls help prevent these issues. They allow you to: +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 query cost +## Estimating operation cost To measure a query's complexity, you typically: -1. Parse the incoming query into a GraphQL document. +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 query if it exceeds a maximum allowed complexity. +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 query passes these checks. +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 -The `graphql-query-complexity` package calculates query cost by walking the AST. Here's a -simple example using `simpleEstimator`, which assigns a flat cost to every field: +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'; @@ -82,13 +103,13 @@ 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 queries early. +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`. +`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: @@ -119,6 +140,12 @@ const UserType = new GraphQLObjectType({ 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 @@ -158,18 +185,14 @@ 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 fallback approach allows you to define detailed logic for specific fields and use -a default cost for everything else. +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 -To enforce complexity limits automatically, you can use `createComplexityRule` from -the same package. This integrates with GraphQL.js validation and prevents execution of -overly complex queries. - -Here's how to include it in your server's execution flow: +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'; @@ -207,24 +230,33 @@ const result = await graphql({ console.log(result); ``` -If the query exceeds the defined complexity limit, GraphQL.js will return a validation -error and skip execution. + + 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: -This approach is useful when you want to apply global complexity rules without needing -to modify resolver logic or add separate middleware. +- 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 -- Set conservative complexity limits at first, and adjust them based on observed usage. -- Use field-level estimators to better reflect real backend cost. -- Log query complexity in development and production to identify inefficiencies. -- Apply stricter limits for public or unauthenticated clients. -- Combine complexity limits with depth limits, persisted queries, or operation -whitelisting for stronger control. +- Use trusted documents in production when possible. +- Use complexity analysis as a development-time safeguards. +- 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 static analysis tool for measuring query cost in GraphQL.js servers +- [`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/) From f39913b24d3406e5238363377af5a9e25d9100f2 Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Wed, 21 May 2025 17:36:12 -0400 Subject: [PATCH 3/6] spellcheck --- cspell.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell.yml b/cspell.yml index ab32211f7c..a40df4f889 100644 --- a/cspell.yml +++ b/cspell.yml @@ -118,3 +118,4 @@ words: - XXXF - bfnrt - wrds + - precompiled From 1f7f50dc071b1122567e1ade765a6c89572ccddd Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Tue, 27 May 2025 17:24:18 -0400 Subject: [PATCH 4/6] fix prettier error --- cspell.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/cspell.yml b/cspell.yml index 7fc8d5ea34..bf708296b2 100644 --- a/cspell.yml +++ b/cspell.yml @@ -120,4 +120,3 @@ words: - XXXF - bfnrt - wrds - \ No newline at end of file From 9a0003193f5745743ee28c77832ef3dd79ec3758 Mon Sep 17 00:00:00 2001 From: Benjie Date: Thu, 29 May 2025 15:22:43 +0100 Subject: [PATCH 5/6] Apply suggestions from code review --- website/pages/docs/operation-complexity-controls.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/pages/docs/operation-complexity-controls.mdx b/website/pages/docs/operation-complexity-controls.mdx index b37e672687..af61f28b2b 100644 --- a/website/pages/docs/operation-complexity-controls.mdx +++ b/website/pages/docs/operation-complexity-controls.mdx @@ -14,7 +14,7 @@ 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](https://graphql.org/learn/persistence/) + 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. @@ -28,7 +28,7 @@ at the schema. Without safeguards, clients could: - Request deeply nested object relationships -- Use recursive fragments to multiply field resolution +- 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 @@ -246,8 +246,8 @@ useful, just in a different way. You can run it at build time to: ## Best practices -- Use trusted documents in production when possible. -- Use complexity analysis as a development-time safeguards. +- 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. From fe7dbe29ff87b6178590feab75d09cb1769cdc9b Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 29 May 2025 15:25:19 +0100 Subject: [PATCH 6/6] Add missing import --- website/pages/docs/operation-complexity-controls.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/pages/docs/operation-complexity-controls.mdx b/website/pages/docs/operation-complexity-controls.mdx index af61f28b2b..9aff4dc67e 100644 --- a/website/pages/docs/operation-complexity-controls.mdx +++ b/website/pages/docs/operation-complexity-controls.mdx @@ -2,6 +2,8 @@ title: Operation Complexity Controls --- +import { Callout } from 'nextra/components' + # Operation Complexity Controls GraphQL gives clients a lot of flexibility to shape responses, but that