How to Prevent GraphQL Query Batching Attacks
A GraphQL query batching attack exploits a server's ability to process multiple operations within a single HTTP request, allowing malicious actors to circumvent standard rate limiters, conduct brute-force attacks, or initiate Denial of Service (DoS) conditions. In high-security environments, securing endpoints requires a layered defense model that inspects both the HTTP transport layer and the internal GraphQL execution pipeline. This guide outlines the primary architectural mechanisms and configurations necessary to effectively neutralize query batching vulnerabilities.
Disabling HTTP Batch Execution
The most direct mitigation against batching attacks is disabling batch processing entirely if the application does not strictly require it. By default, many GraphQL servers (such as Apollo Server or Express-GraphQL) support parsing JSON arrays containing multiple operations:
[
{"query": "query { getUser(id: 1) { name } }"},
{"query": "query { getUser(id: 2) { name } }"}
]Disabling array-based batching at the server configuration level ensures the engine only parses a single operation per HTTP payload, immediately rejecting array payloads with an HTTP 400 Bad Request.
Enforcing Operation-Level Rate Limiting
Traditional Web Application Firewalls (WAFs) and API gateways evaluate rate limits per HTTP request. An attacker can send a single HTTP request containing hundreds of batched operations, bypassing IP-based network throttling.
To prevent this:
- Count Batched Operations: Modify rate-limiting middleware to parse the request body and count the number of distinct operations inside the batch.
- Deduct from the Rate Limit Pool: Decrement the client's rate-limit allowance by the total number of operations, not simply by 1 for the HTTP transaction.
- Throttle and Terminate: Drop or delay the request immediately if the total operation count exceeds the user's allocated threshold.
Calculating Cumulative Query Complexity
Query batching is often combined with resource-heavy fields to cause CPU or memory exhaustion. Calculating query complexity assigns a predefined cost to each requested field and directive before execution begins.
In a batched scenario:
- The complexity calculator must sum the complexity score of every query included in the batch.
- If the cumulative cost across all operations exceeds the allowed execution budget, the entire request must be rejected.
- High-cost queries must not be allowed to execute sequentially if the aggregate footprint surpasses system thresholds.
Strict Array Length and Field Count Limits
If batching is necessary for client performance (e.g., mobile clients reducing round-trips), enforce strict upper bounds:
- Batch Size Cap: Restrict the maximum number of items in a JSON array to a low integer (e.g., 5 to 10 operations).
- Alias Defense: Batching can also occur within a
single query using field aliases (e.g.,
user1: getUser(id:1),user2: getUser(id:2)). Enforce limits on the maximum number of aliases or identical fields allowed per query document to prevent alias-based batching.
Implementing Persisted Queries (Query Allowlisting)
Persisted queries provide the strongest defense against arbitrary query generation and unauthorized batching.
With persisted queries:
- Client operations are hashed and registered at build time into an approved database or key-value store.
- The client sends only the query hash (such as a SHA-256 string) instead of the raw GraphQL string.
- The server looks up the hash; if it is not present in the allowlist, execution is denied.
When combined with strict batch limits, persisted queries prevent attackers from inserting unexpected batch payloads or brute-forcing authentication mutations, because arbitrary queries are never executed.
Enforcing Strict Request Timeouts and Abort Signals
To mitigate the impact of batches that pass initial validation,
high-security systems implement execution-level timeouts. Using
cancellation tokens or context timeouts (such as Node.js
AbortController or Go's context.WithTimeout),
the server strictly terminates database connections and resolver
execution if the total processing time of a batched request exceeds an
established threshold.