← All news
security4 min read

GraphQL DoS: what happens when AI skips complexity limits

By Seaworthy · July 7, 2026

Glowing blue and green network nodes on a black background, resembling an abstract molecular or graph structure

Photo by Ecliptic Graphic on Unsplash

The query you forgot to block

Everyone knows to disable GraphQL introspection in production. That advice is everywhere. This article is about something different: query depth and complexity attacks. Far less discussed, and far more likely to be missing from AI-generated APIs.

The setup is simple. Your schema has a User type with a posts field, and a Post type with an author field that returns a User. Circular reference. This is normal GraphQL modeling.

An attacker sees that and writes this:

query {
  user(id: "1") {
    posts {
      author {
        posts {
          author {
            posts {
              author {
                posts {
                  author {
                    posts {
                      author {
                        id
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

That is 11 levels deep. Automate it to 100 levels and send it over HTTP. No authentication required. The server has to parse and resolve the entire thing before it can respond. If your resolvers hit a database, each level fires more queries. A single request can pin your server for seconds.

This is not the n+1 problem

People conflate this with n+1. They are different.

n+1 is a performance problem: resolving a list of N posts fires N separate database queries for their authors, because the resolver was not written with batching in mind. It is caused by your own code under normal usage. The fix is DataLoader or a smarter resolver.

Depth attacks are resource exhaustion via a single malicious query. You cannot fix them with batching. The query itself is the weapon. The attacker controls it, not you.

Defence 1: depth limiting

The simplest fix is to reject queries that exceed a depth threshold. The graphql-depth-limit package does exactly this.

import depthLimit from 'graphql-depth-limit';
import { ApolloServer } from '@apollo/server';

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

Any query deeper than 10 levels gets rejected at validation time, before any resolver runs. You can tune the limit per schema. A public blog API probably does not need more than 5 levels. An internal tool with complex relationships might need 15. Pick a number and set it.

Defence 2: complexity limiting

Depth alone is not enough. An attacker who knows your limit can write a very wide query instead: a single level that fetches hundreds of fields, each expensive to resolve. Complexity limiting assigns a cost to each field and rejects queries that exceed a total budget.

import { ApolloServer } from '@apollo/server';
import { createComplexityLimitRule } from 'graphql-query-complexity';

const complexityRule = createComplexityLimitRule(1000, {
  scalarCost: 1,
  objectCost: 2,
  listFactor: 10,
});

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

Here list fields multiply the cost by 10, object fields cost 2, scalars cost 1. A query fetching a list of 100 users each with 10 fields will hit the budget fast. You can tune costs per field if you need finer control. The library also supports custom cost functions for unusual schema shapes.

Timeouts as a secondary line

Even with depth and complexity limits in place, a sophisticated attacker might find queries that slip through with valid cost but expensive execution. Set a server-level timeout as a backstop.

With Apollo Server you can wrap resolvers individually, or enforce it at the HTTP layer: a reverse proxy like nginx can terminate requests that exceed a configured duration. This is not a primary defence, but it limits the blast radius of anything that gets through.

Why AI-generated GraphQL APIs skip this

I have looked at a lot of generated GraphQL scaffolding. None of it includes depth or complexity limits. Not from Claude, not from GPT. Apollo Server does not add them by default. Neither does Pothos, Nexus, or TypeGraphQL.

This makes sense from a code generation perspective: the defaults ship, the code runs, the tests pass. Limits that prevent certain queries from running feel like optional hardening, not a requirement.

They are a requirement. Any public GraphQL API without depth and complexity limits is accepting arbitrary computation from anonymous clients. That is not a tradeoff, it is an oversight.

Add graphql-depth-limit and graphql-query-complexity before you ship. Two packages, about ten minutes of configuration.

Seaworthy flags missing depth and complexity validation rules as part of its resilience checks.

This article was generated by AI and summarises publicly available sources.

From the makers of Seaworthy

Seaworthy scans your repo for the issues covered in articles like this one.

Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.

$ npx seaworthycode