Depth & Complexity
We have settled who is asking and whether they are allowed. Now the third question: how expensive is this query? A request can be perfectly authenticated, fully authorized, and still bring a server to its knees — because GraphQL lets clients author the shape of the work, and a small query string can describe an enormous amount of it.
How a tiny query becomes a denial of service
Section titled “How a tiny query becomes a denial of service”Two features of a connected graph make abuse easy:
- Deep nesting. If
Userhasfriends: [User!]!, thenuser → friends → friends → friends → …is legal SDL forever. Each level multiplies the rows the server must fetch. Ten levels of a modest fan-out is millions of objects from a few lines of query. - Aliases. A client may request the same expensive field many times under different names:
a: search(...) b: search(...) c: search(...). The query stays short, but the server runs the work once per alias.
# Short to write, ruinous to execute: nesting plus aliasing.query Abuse { user(id: "1") { friends { # level 1 friends { # level 2 friends { # level 3 ... and on, and on a: posts { title } b: posts { title } # same field, billed twice via aliases } } } }}You cannot fix this with authentication or authorization — the attacker may be a legitimate, logged-in user. You fix it by measuring cost and refusing what is too expensive before execution.
flowchart LR
P["parse: query string -> AST"] --> V["validate against schema"]
V --> Depth{"Depth within limit?"}
Depth -->|"No"| Rej["Reject (no resolver runs)"]
Depth -->|"Yes"| Cost{"Complexity within budget?"}
Cost -->|"No"| Rej
Cost -->|"Yes"| Exec["Execute with a timeout"]
Exec -->|"too slow"| Abort["Abort on timeout"]
Exec --> R["Response"] Three defenses, cheapest first
Section titled “Three defenses, cheapest first”- Depth limiting. Walk the parsed query and reject it if nesting exceeds a fixed maximum (say, 7). It is blunt but cheap and catches the recursive-nesting attack outright. This is the easiest control to add and the one we will compute live below.
- Complexity (cost) analysis. Assign each field a cost — a flat number, or a formula based on arguments like
first: 100for a paginated list — sum the costs for the whole query, multiply through nesting, and reject anything over a budget. This handles wide and deep queries and accounts for aliases, since each alias is a separate selection that adds to the total. - Timeouts. A backstop for whatever slips through static analysis: cap wall-clock execution (and per-data-source calls) so a query that turns out expensive at runtime is aborted rather than allowed to run forever.
Libraries such as graphql-depth-limit and graphql-query-complexity implement the first two as validation rules, so the query is rejected during validation — no resolver ever runs. The cost rule reads like this:
import { createComplexityRule, simpleEstimator } from 'graphql-query-complexity';
const complexityRule = createComplexityRule({ maximumComplexity: 1000, estimators: [ // A list field costs (childComplexity * the requested page size). simpleEstimator({ defaultComplexity: 1 }), ], onComplete: (cost) => { if (cost > 1000) throw new Error('Query is too complex: ' + cost); },});// Pass complexityRule in the server's validationRules so it runs before execution.Computing depth from a parsed query, live
Section titled “Computing depth from a parsed query, live”Depth limiting is something you can implement in a few lines, and the heart of it is the GraphQL parse function — the same one a server uses. It turns a query string into an AST (abstract syntax tree). Walk the selectionSets, and the deepest chain of nested selections is the query’s depth. The demo below parses three queries and measures each, then applies a limit of 5. Press Run.
The output ranks the three queries by depth and applies the limit. The shallow and medium queries pass; the abusive one — five levels of friends deep — exceeds the limit of 5 and is rejected. Note that no resolver ran: we worked purely on the parsed AST, exactly as a validation rule does, so a malicious query is thrown out before it can cost anything. Real depth-limit libraries do the same walk; complexity analysis extends it by weighting each field instead of just counting nesting levels.