GraphQL security hinges on controlling batching attacks. Learn how to prevent resource exhaustion and DoS by enforcing query depth and complexity limits.
During a recent refactor of a client's public-facing API, I noticed our error logs spiking with mysterious timeouts. It turned out that a single client was sending a batch of 500 identical, expensive queries in one HTTP request, effectively bypassing our standard rate limiter.
If you’re running a GraphQL server, you’re likely vulnerable to this unless you’ve explicitly hardened your execution engine. GraphQL’s flexibility is a double-edged sword; it allows clients to request exactly what they need, but it also gives them the power to request everything at once.
Most GraphQL servers—whether using Apollo Server, Yoga, or custom implementations—support batching by default. It’s a performance feature: instead of firing ten separate HTTP requests, the client sends an array of queries.
The problem arises when an attacker treats that array as a weapon. If your rate limiter counts total HTTP requests, sending one array of 200 "heavy" queries counts as exactly one request. Your server will happily attempt to resolve all 200 queries concurrently, leading to rapid CPU saturation and memory pressure. This is a classic case of resource exhaustion that often goes unnoticed until the service becomes unresponsive.
We first tried simple request-based rate limiting, but it didn't solve the issue because the attacker just grouped more queries into fewer requests. We realized we needed to analyze the intent of the query before execution.
The most immediate step for GraphQL security is enforcing query depth limits. Most malicious queries involve deeply nested relationships—like asking for a user, then their posts, then the comments on those posts, then the authors of those comments, and so on.
You can use libraries like graphql-depth-limit to reject queries that exceed a specific nesting level. For most applications, a depth of 5 to 7 is more than enough for legitimate use cases.
JAVASCRIPTimport depthLimit from CE9178">'graphql-depth-limit'; const server = new ApolloServer({ typeDefs, resolvers, validationRules: [depthLimit(5)], });
If a client tries to go deeper, the server returns a validation error immediately without ever touching your database or resolvers. It’s a cheap, effective filter.
Depth limiting is good, but it doesn't account for "wide" queries. A query might be shallow but request 1,000 fields across 50 different entities. This is where API Security: Preventing Resource Exhaustion with Query Complexity Analysis becomes essential.
By assigning a "cost" to each field in your schema, you can calculate the total weight of a query before it runs.
When you combine this with batching, you must calculate the sum of the costs across the entire array. If you allow a maximum cost of 500 per request, a batch of 10 queries, each costing 60, should be rejected.
Circular queries are a specific variant of DoS prevention where a client traverses a graph cycle (e.g., A -> B -> A). While depth limiting catches these, it’s worth noting that your resolvers should always implement data loaders to prevent the N+1 problem.
If you aren't using something like DataLoader in Node.js, even a "simple" query can trigger thousands of database hits. I've seen systems fall over because a recursive query triggered a cascade of database connection pool exhaustion. Remember, GraphQL security: Preventing Improper Authorization in Resolvers is just as critical; if an attacker can force your server to resolve a circular graph, they might also be probing for authorization bypasses.
To harden your API, I recommend this order of operations:
I’m still not entirely comfortable relying solely on complexity scores. They are inherently subjective—what if a database index changes or a cache gets evicted? The "cost" of a resolver can fluctuate wildly.
Next time, I'd like to implement a more dynamic "cost" system that monitors actual execution time per resolver in production and feeds that data back into the complexity calculator. It’s a complex piece of engineering, but for high-traffic APIs, it’s probably the only way to stay ahead of resource exhaustion. Keep your limits tight, monitor your execution times, and don't assume your GraphQL server is safe just because it’s behind a standard WAF.
GraphQL security starts at the resolver level. Learn how to prevent improper authorization and data leaks by enforcing access control on individual fields.
Read moreWebSocket security starts with preventing CSWSH. Learn how to validate origins and secure your real-time connections in Node.js and Laravel applications.