| title | GraphQL Protection |
|---|---|
| sidebar_position | 2 |
The gateway can analyze GraphQL queries in transit to enforce depth limits, complexity limits, introspection control, and per-operation-type rate limits. This protects GraphQL backends from abusive or excessively expensive queries.
Enable GraphQL on a route that fronts a GraphQL backend:
routes:
- id: "graphql"
path: "/graphql"
backends:
- url: "http://graphql-server:4000"
graphql:
enabled: true
max_depth: 10
max_complexity: 100
introspection: false
operation_limits:
query: 100 # queries per second
mutation: 10 # mutations per second
subscription: 5 # subscriptions per secondGraphQL analysis only activates for POST requests with Content-Type: application/json. Other requests pass through unchanged.
Prevents deeply nested queries that can cause exponential backend work:
graphql:
enabled: true
max_depth: 10 # 0 = unlimitedA query like { user { posts { comments { author { posts { ... } } } } } } has depth 5. Queries exceeding max_depth are rejected with a GraphQL error response.
Limits the total complexity score of a query. Each field selection adds 1 to the complexity count:
graphql:
enabled: true
max_complexity: 200 # 0 = unlimitedBlock introspection queries (__schema, __type) in production:
graphql:
enabled: true
introspection: false # default: falseWhen disabled, introspection queries are rejected with a GraphQL error response.
Rate limit by GraphQL operation type (query, mutation, subscription):
graphql:
enabled: true
operation_limits:
query: 100 # max queries per second
mutation: 10 # max mutations per second
subscription: 5 # max subscriptions per secondEach operation type has its own independent token bucket. Exceeded operations return a GraphQL error response.
APQ reduces bandwidth by allowing clients to send a hash of a previously registered query instead of the full query text. This follows the Apollo APQ protocol.
graphql:
enabled: true
persisted_queries:
enabled: true
max_size: 1000 # LRU cache max entries (default 1000)-
First request (register): Client sends both the query and its SHA-256 hash in the
extensionsfield. The gateway verifies the hash matches, stores it in the LRU cache, and forwards the request. -
Subsequent requests (lookup): Client sends only the hash (no query). The gateway looks up the hash in the cache, substitutes the full query, and forwards it to the backend.
-
Cache miss: If the hash is not found, the gateway returns a
PersistedQueryNotFounderror (HTTP 200, per Apollo protocol). The client should retry with the full query + hash.
{
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"
}
}
}- the gateway verifies that the SHA-256 hash matches the query text before storing, preventing cache poisoning.
- The LRU cache evicts least recently used queries when full.
GraphQL clients (Apollo, Relay, urql) can batch multiple operations into a single HTTP request by sending a JSON array instead of a single object. The gateway detects batched requests and validates each query individually.
graphql:
enabled: true
batching:
enabled: true
max_batch_size: 10 # max queries per batch (default 10, 0 = unlimited)
mode: "pass_through" # "pass_through" or "split" (default "pass_through")A request body starting with [ is treated as a batch. Each element must be a standard GraphQL request object ({query, variables, operationName, extensions}). If batching is not enabled and an array is received, the gateway returns a 400 error.
Every query in a batch is individually validated against depth limits, complexity limits, introspection control, and per-operation rate limits. If any query fails validation, the entire batch is rejected with an error referencing the query index (e.g., "query[2]: depth 15 exceeds maximum 10").
APQ (Automatic Persisted Queries) resolution also works per-query within a batch — each element can use hash-only lookups independently.
Pass-through mode (mode: "pass_through", default): The gateway validates all queries, resolves any APQ hashes, then forwards the entire JSON array to the backend. Use this when the backend natively supports batch requests.
Split mode (mode: "split"): The gateway fans out each query as an individual HTTP request through the full downstream middleware chain (cache, circuit breaker, etc.), then merges the responses into a JSON array. Use this when the backend only handles single queries, or when you want each query to benefit from per-query caching and circuit breaking.
An empty array [] returns an empty array response [] with status 200.
Batch metrics are exposed via the admin /graphql endpoint:
batching.requests_total— number of batch requests receivedbatching.queries_total— total individual queries across all batchesbatching.size_rejected— batches rejected for exceedingmax_batch_size
When used with caching, GraphQL analysis enhances cache keys with the operation name and a hash of query variables. This enables caching of GraphQL POST requests for query operations (mutations and subscriptions always bypass cache).
All GraphQL errors are returned in the standard GraphQL JSON format:
{
"errors": [
{
"message": "query depth 15 exceeds maximum allowed depth of 10"
}
]
}| Field | Type | Description |
|---|---|---|
graphql.enabled |
bool | Enable GraphQL analysis |
graphql.max_depth |
int | Max query nesting depth (0 = unlimited) |
graphql.max_complexity |
int | Max query complexity score (0 = unlimited) |
graphql.introspection |
bool | Allow introspection queries (default false) |
graphql.operation_limits |
map | Per-type rate limits: query, mutation, subscription |
graphql.persisted_queries.enabled |
bool | Enable Automatic Persisted Queries |
graphql.persisted_queries.max_size |
int | LRU cache max entries (default 1000) |
graphql.batching.enabled |
bool | Enable query batching |
graphql.batching.max_batch_size |
int | Max queries per batch (default 10, 0 = unlimited) |
graphql.batching.mode |
string | "pass_through" or "split" (default "pass_through") |
See Configuration Reference for all fields.