GraphQL Batching Attacks: The DoS Vector Hiding in Plain Sight
How attackers exploit GraphQL's batching feature to bypass rate limits, brute-force credentials, and exhaust API resources—and why standard WAFs can't stop them.
GraphQL Batching Attacks: The DoS Vector Hiding in Plain Sight
In March 2026, a fintech startup running a GraphQL API discovered their authentication endpoint was processing 50,000 login attempts per second—all from a single IP address. Their WAF showed nothing unusual. Rate limiting was "working." But attackers weren't sending 50,000 HTTP requests. They were sending one.
Welcome to the world of GraphQL batching attacks, where the flexibility that makes GraphQL powerful becomes its greatest vulnerability.
The Batching Trap: When Convenience Becomes Exploitation
GraphQL's batching feature was designed to solve the N+1 query problem. Instead of making multiple HTTP round trips, clients bundle operations into a single request:
[
{"query": "query { user(id: 1) { name } }"},
{"query": "query { user(id: 2) { name } }"},
{"query": "query { user(id: 3) { name } }"}
]
The server processes them sequentially, returning an array of results. Elegant, efficient—and absolutely devastating when weaponized.
Attackers quickly realized that most rate limiters count HTTP requests, not GraphQL operations. A single POST request containing 100 login attempts looks like one request to your infrastructure but executes as 100 authentication attempts against your database. Standard WAF rules don't inspect JSON payloads for nested query arrays. IP-based blocking becomes meaningless when the "attack volume" shows up as 10 requests per minute.
The OTP Bypass Reality
Batching attacks don't just brute-force passwords. Security researchers have demonstrated bypassing two-factor authentication by sending all 1,000,000 possible 6-digit OTP codes in a single batched request. The server validates each sequentially until one succeeds—all within the "allowed" request rate.
Beyond Batching: The Resource Exhaustion Arsenal
Disabling batching doesn't solve the problem. Attackers have multiple vectors to achieve the same outcome:
Alias Overloading When batching is disabled, aliases provide an alternative path:
query {
user1: user(id: 1) { name email }
user2: user(id: 2) { name email }
user3: user(id: 3) { name email }
# ... repeat 100 times
}
The server processes each alias as a separate resolver invocation. One request, N operations.
Field Duplication Even simpler—duplicate fields force redundant computation:
query {
user(id: 1) {
name
name
name
# repeated 50 times
}
}
Some GraphQL implementations deduplicate response fields but still execute the resolvers, wasting CPU and database connections.
Circular Fragments GraphQL fragments are client-controlled and can create circular references that expand exponentially:
fragment UserFields on User {
friends {
...UserFields
}
}
query {
user(id: 1) {
...UserFields
}
}
Without depth limiting, this query recursively traverses relationships until resource exhaustion crashes the server.
Case Study: The Hidden Cost of Nested Queries
An e-commerce platform experienced intermittent API outages during peak traffic. Investigation revealed attackers sending complex queries with 15+ levels of nesting: product -> category -> products -> reviews -> user -> orders -> products... Each request triggered thousands of database queries. The "small" API load (200 req/s) translated to 200,000+ SQL queries per second, overwhelming the PostgreSQL connection pool and causing cascading failures across the platform.
The fix cost $47,000 in emergency infrastructure scaling before proper query complexity analysis was implemented.
Defense in Depth: Securing GraphQL APIs
Protecting GraphQL requires moving beyond traditional HTTP-centric security models. Here's a practical defense strategy:
1. Query Complexity Analysis
Assign cost scores to fields and reject queries exceeding thresholds:
# Python example using graphql-cost-analysis concepts
COMPLEXITY_LIMIT = 1000
field_costs = {
'User': {'friends': 10, 'orders': 5},
'Order': {'products': 5, 'shipping': 1},
}
def calculate_complexity(query_ast):
total = 0
for field in query_ast.selections:
cost = field_costs.get(field.type, {}).get(field.name, 1)
total += cost * calculate_complexity(field)
return total
2. Depth Limiting
Prevent circular and deeply nested queries:
// Node.js using graphql-depth-limit
const depthLimit = require('graphql-depth-limit');
const { createYoga } = require('graphql-yoga');
const yoga = createYoga({
schema,
validationRules: [depthLimit(10)] // Max 10 levels deep
});
3. Operation-Level Rate Limiting
Count GraphQL operations, not HTTP requests:
# Example Envoy proxy configuration
rate_limits:
- actions:
- request_headers:
header_name: "x-graphql-operation-count"
descriptor_key: "graphql_ops"
limit:
unit: minute
requests_per_unit: 50 # Max 50 operations per minute
4. Persistent Query Whitelisting
Only allow pre-registered queries in production:
// Using @apollo/persisted-query-lists
const { PersistedQueryList } = require('@apollo/persisted-query-lists');
const allowList = new PersistedQueryList({
queries: require('./query-manifest.json')
});
Query Cost Analysis
Assign complexity scores to fields based on database impact. Reject queries exceeding cost thresholds before execution.
Depth & Breadth Limits
Enforce maximum query depth and limit the number of fields per selection to prevent resource exhaustion attacks.
Timeout Controls
Set aggressive query execution timeouts. Kill queries running longer than 5 seconds to prevent slowloris-style attacks.
Trusted Documents
Disable arbitrary queries in production. Only execute pre-registered, hashed queries known at build time.
The Bottom Line
GraphQL's flexibility is a double-edged sword. The same features that make it developer-friendly—batching, nested queries, client-specified fields—create attack surfaces invisible to traditional security tooling. Standard WAFs, IP-based rate limiting, and HTTP-centric monitoring won't save you.
The vulnerabilities disclosed in early 2026—including Check Point Research's findings on Claude Code's API key exfiltration and the ongoing GraphQL DoS campaigns targeting fintech—demonstrate that attackers are actively exploiting these gaps.
Audit your GraphQL APIs today:
- Review rate limiting logic—are you counting operations or requests?
- Test query complexity limits with real attack patterns
- Disable introspection and GraphiQL in production
- Implement persistent query whitelisting
- Monitor for alias-heavy queries and field duplication
Your next breach might come in a single POST request containing a thousand malicious operations. Make sure you're counting what matters.
Decode JWTs Without Exposing Secrets
Stop pasting your tokens into online decoders that log your payload. Use our fully client-side JWT decoder to inspect headers and payloads without sending data to any server.
Open JWT Decoder →