Diagnosing and Controlling GraphQL Query Complexity: A Practical Guide
Learn how to detect, measure, and cap GraphQL query complexity to prevent performance regressions and DoS attacks. Follow our step‑by‑step diagnostic guide, example configs, and escalation rules.
15 Jun 2026, 07:02 UTC

Recognizing the Problem
When a GraphQL endpoint starts to slow dramatically or even refuses to respond, the first clue is often a sudden spike in request latency or a denial‑of‑service (DoS) style error. In most cases the culprit is a client sending a query that is deeply nested, contains many fields, or invokes expensive resolvers. Without any guardrails the server must walk the entire query tree, which can exhaust CPU, memory, or database connections.
Short Cause/Diagnostic Table
| Condition | Likely Cause | Initial Check |
|---|---|---|
| Sudden latency increase | Deep or expensive query | Inspect request.query in logs |
| Server errors (5xx) on valid queries | Complexity threshold hit | Check error message for "query complexity" |
| High CPU/memory usage | Unbounded recursion in resolvers | Profile resolver execution time |
| Repeated identical slow queries | Caching not applied | Verify cache headers and backend cache hits |
Ordered Checks
- Enable Complexity Counter
In Apollo Server (Node.js) you can add a
complexityplugin that traverses the AST before execution.const { ApolloServer } = require("apollo-server"); const { createComplexityLimitPlugin } = require("graphql-query-complexity"); const server = new ApolloServer({ typeDefs, resolvers, plugins: [ createComplexityLimitPlugin({ maximumComplexity: 200, variables: { /* map variables to cost if needed */ }, onCost: ({ cost }) => console.log(`Query cost: ${cost}`), formatError: (cost) => new Error(`Query is too complex: ${cost}`), }), ], });Run this in the
devandstagingenvironments first. The plugin logs every query’s cost and rejects those exceedingmaximumComplexitywith a clear 400 error. - Add Depth Limiting
Depth is a simpler metric that caps how many nested levels a query can have.
const { createDepthLimitPlugin } = require("graphql-depth-limit"); server.plugins.push( createDepthLimitPlugin({ maximumDepth: 10 }) );Depth checks occur before cost calculation, so they protect against runaway parsing overhead.
- Identify Expensive Resolvers
Assign a higher cost multiplier to fields that hit the database or call external services.
const costMapping = { user: 10, posts: 5, comments: 3, }; // In the plugin configuration variables: costMapping, - Verify Enforcement
Send a deliberately deep query from a GraphQL client:
query { user(id: "1") { posts { comments { replies { text } } } } }The server should respond with HTTP 400 and a message like "Query is too complex: 200". Check the logs for the recorded cost.
- Monitor in Production
Use a log aggregator (e.g., Loki, CloudWatch) to surface rejected queries. Set an alert if more than 5% of requests are blocked by the complexity plugin.
Tied Fixes for Findings
- High depth → Increase
maximumDepthcautiously or refactor schema to flatten nesting. - High cost → Reduce
maximumComplexityor lower multipliers for expensive resolvers. - Frequent legitimate high‑cost queries → Move heavy logic to background jobs or pre‑compute fields.
- Unnecessary field selection → Encourage clients to use fragments or provide a
__schemaintrospection guide.
Escalation Criteria
- If the server still experiences DoS symptoms after applying depth and complexity limits, consider adding a rate‑limit middleware (e.g.,
express-rate-limit). - For critical services, deploy a
graphql-middlewarethat short‑circuits queries exceeding a hard threshold and returns a 429 status. - When developers report legitimate queries being blocked, review the cost mapping and adjust multipliers or expose a
costReportendpoint for fine‑grained analysis.
Limitations & Practical Checks
- Complexity analysis adds parsing overhead; keep the cost function lightweight.
- Mis‑calibrated multipliers can block valid work; run a regression test suite with known query depths to validate.
- Some resolvers may have unpredictable cost (e.g., variable‑size external API responses). In those cases, use a conservative upper bound.
- Always test in a staging environment before rolling to production.
To verify the result, run npm test with a test harness that submits a set of queries, captures the returned HTTP status, and asserts the expected cost value. A successful test should see a 200 for low‑cost queries and a 400 for those exceeding the limit.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.