GraphQL Query Complexity and Depth Limiting

GraphQL's power is also its security risk. A single query can ask for deeply nested data or thousands of related items, making your server do enormous amounts of work. Query complexity and depth limiting protect your server from abusive or accidental queries that could bring it down.

The Attack Query Problem

  A valid but devastating query:
  ────────────────────────────────
  {
    users {                    ← All users (millions?)
      friends {                ← Each user's friends
        friends {              ← Each friend's friends
          friends {            ← Each friend's friends' friends
            posts {            ← All posts for each
              comments {       ← All comments for each post
                author {
                  friends { name }
                }
              }
            }
          }
        }
      }
    }
  }

  This query is syntactically valid.
  It could trigger billions of database calls.
  Without limits: server crashes or DB melts.

Depth Limiting

Depth limiting rejects any query where the nesting level exceeds a defined maximum. Setting a depth limit of 5 means a query can nest at most 5 levels deep. Anything deeper is rejected before any resolver runs.

  npm install graphql-depth-limit

  import depthLimit from 'graphql-depth-limit';

  const server = new ApolloServer({
    typeDefs,
    resolvers,
    validationRules: [depthLimit(5)]
  });

  Depth counting:
  ────────────────
  { users {            ← depth 1
    friends {          ← depth 2
      friends {        ← depth 3
        friends {      ← depth 4
          name         ← depth 5 ✓ allowed
          posts {      ← depth 6 ✗ REJECTED
          }
        }
      }
    }
  }

Query Complexity Limiting

Depth limits alone are not enough. A query that fetches 10,000 users with just 2 levels of nesting can still overload your server. Complexity limiting assigns a cost to each field and rejects queries whose total cost exceeds a threshold.

  npm install graphql-query-complexity

  import {
    createComplexityLimitRule,
    simpleEstimator,
    fieldExtensionsEstimator,
  } from 'graphql-query-complexity';

  const server = new ApolloServer({
    typeDefs,
    resolvers,
    validationRules: [
      createComplexityLimitRule(1000, {       ← Max complexity: 1000
        estimators: [
          fieldExtensionsEstimator(),
          simpleEstimator({ defaultComplexity: 1 })
        ]
      })
    ]
  });

  // Assign costs in schema extensions:
  type Query {
    users: [User]     @complexity(value: 5, multipliers: ["limit"])
    user(id: ID!): User @complexity(value: 1)
  }

  Complexity calculation example:
  ────────────────────────────────
  { users(limit: 100) { name posts { title } } }

  users:          5  × 100 (limit multiplier) = 500
  User.name:      1  × 100                    = 100
  User.posts:     1  × 100                    = 100
  Post.title:     1  × 100 (estimated posts)  = 100
                                       Total: = 800 ✓ under 1000

Disabling Introspection in Production

GraphQL introspection lets anyone query your entire schema. In development this is great for tools. In production it hands attackers a roadmap to your API. Disable it unless you have a reason to keep it on.

  const server = new ApolloServer({
    typeDefs,
    resolvers,
    introspection: process.env.NODE_ENV !== 'production'
  });

Query Timeout

Even with complexity and depth limits, set a wall-clock timeout so a slow resolver cannot hold a connection open indefinitely.

  // Express middleware example:
  app.use('/graphql', timeout('10s'), (req, res, next) => {
    if (!req.timedout) next();
  });

Key Points

  • GraphQL queries are valid syntactically but can be destructively expensive without limits.
  • Depth limiting rejects queries nested deeper than a defined maximum level.
  • Complexity limiting assigns a cost to each field and rejects queries whose total cost exceeds a threshold.
  • Disable introspection in production to prevent schema discovery by attackers.
  • Set a request timeout as a final safety net against runaway resolvers.

Leave a Comment