Request Limits
Protect your router from requests exceeding network, parser, and operation-based limits
For enhanced security, the GraphOS Router can reject requests that violate any of the following kinds of limits:
Operation-based semantic limits
Network-based limits
Parser-based lexical limits
1limits:
2 # Network-based limits
3 http_max_request_bytes: 2000000 # Default value: 2 MB
4 http1_max_request_headers: 200 # Default value: 100
5 http1_max_request_buf_size: 800kb # Default value: 400kib
6
7 # New middleware-level header limits
8 http_max_request_headers: 100 # Limit total number of headers in a request
9 http_max_header_list_items: 10 # Limit number of values per header field
10
11 # Parser-based limits
12 parser_max_tokens: 15000 # Default value
13 parser_max_recursion: 500 # Default value
14
15 # Operation-based limits (License only)
16 max_depth: 100
17 max_height: 200
18 max_aliases: 30
19 max_root_fields: 20Operation-based limits
You can define operation limits in your router's configuration to reject potentially malicious requests. An operation that exceeds any specified limit is rejected (unless you run your router in warn_only mode).
Setup
To use operation limits, you must run v1.17 or later of the Apollo Router. Download the latest version.
You define operation limits in your router's YAML config file, like so:
1limits:
2 max_depth: 100
3 max_height: 200
4 max_aliases: 30
5 max_root_fields: 20
6
7 # Uncomment to enable warn_only mode
8 # warn_only: trueEach limit takes an integer value. You can define any combination of supported limits.
Supported limits
max_depth
Limits the deepest nesting of selection sets in an operation, including fields in fragments.
The GetBook operation below has depth three:
1query GetBook {
2 book { # Depth 1 (root field)
3 ...bookDetails
4 }
5}
6
7fragment bookDetails on Book {
8 details { # Depth 2 (nested under `book`)
9 ... on ProductDetailsBook {
10 country # Depth 3 (nested under `details`)
11 }
12 }
13}max_height
Limits the number of unique fields included in an operation, including fields of fragments. If a particular field is included multiple times via aliases, it's counted only once.
The GetUser operation below has height three:
1query GetUser {
2 user { # 1
3 id # 2
4 name # 3
5 username: name # Aliased duplicate (not counted)
6 }
7}Each unique field increments an operation's height by one, regardless of that field's return type (scalar, object, or list).
max_aliases
Limits the total number of aliased fields in an operation, including fields of fragments.
The GetUser operation below includes three aliases:
1query GetUser {
2 user {
3 nickname: name # 1
4 username: name # 2
5 handle: name # 3
6 }
7}Each aliased field increments the alias count by one, regardless of that field's return type (scalar, object, or list).
max_root_fields
Limits the number of root fields in an operation, including root fields in fragments. If a particular root field is included multiple times via aliases, each usage is counted.
The following operation includes three root fields:
1query GetTopProducts {
2 topBooks { # 1
3 id
4 }
5 topMovies { # 2
6 id
7 }
8 topGames { # 3
9 id
10 }
11}warn_only mode
If you run your router in warn_only mode, operations that exceed defined limits are not rejected. Instead, the router processes these operations as usual and emits a WARN trace that notes all exceeded limits, like so:
12023-03-15T19:08:23.123456Z WARN apollo_router::operation_limits: max_depth exceeded, max_depth: 3, current_op_depth: 5, operation: "query GetOwnerLocation {cat {owner {location {postalCode}}}}"Running in warn_only mode can be useful while you're testing to determine the most appropriate limits to set for your supergraph.
You can enable or disable warn_only mode in your router's YAML config file, like so:
1limits:
2 warn_only: true # warn_only mode always enabledResponse format for exceeded limits
Whenever your router rejects a request because it exceeds an operation limit, the router responds with a 400 HTTP status code and a standard GraphQL error response body:
1# HTTP 400
2{
3 "data": {},
4 "errors": [
5 {
6 "message": "Maximum height (field count) limit exceeded in this operation",
7 "extensions": {
8 "code": "MAX_HEIGHT_LIMIT"
9 }
10 }
11 ]
12}If you run your router in warn_only mode, the router logs the limit violation but executes the operation as normal, returning a 200 status code with the expected response.
Using telemetry to set operation-based limits
Router telemetry can help you set operation limits, especially when you have a large number of existing operations. You can measure incoming operations over a fixed duration, then use the captured data as a baseline configuration.
Logging values
To log limit information about every operation, you can configure the router with a custom event to log the values of aliases, depth, height, and root_fields for each operation:
1telemetry:
2 instrumentation:
3 events:
4 supergraph:
5 OPERATION_LIMIT_INFO:
6 message: operation limit info
7 on: response
8 level: info
9 attributes:
10 graphql.operation.name: true
11 query.aliases:
12 query: aliases
13 query.depth:
14 query: depth
15 query.height:
16 query: height
17 query.root_fields:
18 query: root_fieldsCollecting metrics
To capture and view metrics to help set your operation limits, you can configure the router to collect custom metrics on the values of aliases, depth, height, and root_fields for each operation:
1telemetry:
2 exporters:
3 metrics:
4 common:
5 views:
6 # Define a custom view because operation limits are different than the default latency-oriented view of OpenTelemetry
7 - name: oplimits.*
8 aggregation:
9 histogram:
10 buckets:
11 - 0
12 - 5
13 - 10
14 - 25
15 - 50
16 - 100
17 - 500
18 - 1000
19 instrumentation:
20 instruments:
21 supergraph:
22 oplimits.aliases:
23 value:
24 query: aliases
25 type: histogram
26 unit: number
27 description: "Aliases for an operation"
28 oplimits.depth:
29 value:
30 query: depth
31 type: histogram
32 unit: number
33 description: "Depth for an operation"
34 oplimits.height:
35 value:
36 query: height
37 type: histogram
38 unit: number
39 description: "Height for an operation"
40 oplimits.root_fields:
41 value:
42 query: root_fields
43 type: histogram
44 unit: number
45 description: "Root fields for an operation"You should also configure the router to export metrics to your APM tool.
Network-based limits
http_max_request_bytes
Limits the amount of data read from the network for the body of HTTP requests, to protect against unbounded memory consumption. This limit is checked before JSON parsing. Both the GraphQL document and associated variables count toward it.
The default value is 2000000 bytes, 2 MB.
Before increasing this limit significantly consider testing performance in an environment similar to your production, especially if some clients are untrusted. Many concurrent large requests could cause the router to run out of memory.
http1_max_request_headers
Limit the maximum number of headers of incoming HTTP1 requests. The default value is 100 headers.
If router receives more headers than the buffer size, it responds to the client with 431 Request Header Fields Too Large.
http1_max_request_buf_size
Limit the maximum buffer size for the HTTP1 connection. Default is ~400kib.
http_max_request_headers
New in v2.6+ - Middleware-level limit for the maximum number of headers in an HTTP request.
This limit is enforced at the application layer (middleware) and applies to all HTTP versions (HTTP/1.1, HTTP/2, etc.). Unlike http1_max_request_headers which is enforced at the HTTP server level, this middleware-level limit provides more consistent behavior across different HTTP protocols.
When not specified, no limit is enforced at the middleware level.
If a request contains more headers than this limit, the router responds with 431 Request Header Fields Too Large:
1{
2 "errors": [
3 {
4 "message": "Request header fields too many",
5 "extensions": {
6 "details": "Request header fields too many",
7 "code": "INVALID_GRAPHQL_REQUEST"
8 }
9 }
10 ]
11}http_max_header_list_items
New in v2.6+ - Middleware-level limit for the maximum number of values allowed per header field.
This limit is enforced when a single header field contains multiple values (e.g., Accept: application/json, text/html, application/xml). If any header field contains more values than this limit, the router responds with 431 Request Header Fields Too Large.
When not specified, no limit is enforced at the middleware level.
Example configuration:
1limits:
2 http_max_header_list_items: 5 # Allow up to 5 values per headerIf a request contains a header with more values than this limit, the router responds with 431 Request Header Fields Too Large:
1{
2 "errors": [
3 {
4 "message": "Request header list too many items",
5 "extensions": {
6 "details": "Request header list too many items",
7 "code": "INVALID_GRAPHQL_REQUEST"
8 }
9 }
10 ]
11}HTTP Header Configuration (server.http)
As an alternative to the legacy http1_* limits above, you can configure HTTP header limits using the server.http section, which provides more comprehensive control over both HTTP/1.1 and HTTP/2 connections:
1server:
2 http:
3 # Maximum size of individual header field (name + value) in bytes
4 # Applies to both HTTP/1.1 and HTTP/2
5 max_header_size: "32kb"
6
7 # Maximum number of headers allowed in a request
8 # Primarily affects HTTP/1.1 connections
9 max_headers: 250
10
11 # Maximum total size of all headers combined
12 # Primarily affects HTTP/2 connections
13 max_header_list_size: "64kb"
14
15 # Header read timeout (applies to all HTTP versions)
16 header_read_timeout: "10s"The server.http configuration takes precedence over the legacy limits.http1_* settings when both are specified. This allows you to:
Set per-header size limits with
max_header_size(similar to Node.js--max-http-header-size)Control header count limits with
max_headersfor HTTP/1.1Set total header payload limits with
max_header_list_sizefor HTTP/2Configure header read timeouts consistently across all HTTP versions
If router receives requests exceeding these limits, it responds with 431 Request Header Fields Too Large.
Middleware-level vs Server-level Header Limits
The Apollo Router provides two approaches for limiting headers:
Server-level limits (
server.http.*and legacylimits.http1_*): Enforced by the underlying HTTP server implementation. These are protocol-specific and enforced early in the request processing pipeline.Middleware-level limits (
limits.http_max_request_headersandlimits.http_max_header_list_items): Enforced by Apollo Router middleware after the HTTP request is parsed but before GraphQL processing. These provide consistent behavior across all HTTP versions and can be dynamically configured.
Choose server-level limits for maximum performance (earlier rejection) and choose middleware-level limits for consistent behavior across HTTP versions and more sophisticated routing logic.
Parser-based limits
parser_max_tokens
Limits the number of tokens a query document can include. This counts all tokens, including both lexical and ignored tokens.
The default value is 15000.
parser_max_recursion
Limits the deepest level of recursion allowed by the router's GraphQL parser to prevent stack overflows. This corresponds to the deepest nesting level of any single GraphQL operation or fragment defined in a query document.
The default value is 500.
In the example below, the GetProducts operation has a recursion of three, and the ProductVariation fragment has a recursion of two. Therefore, the max recursion of the query document is three.
1query GetProducts {
2 allProducts { #1
3 ...productVariation
4 delivery { #2
5 fastestDelivery #3
6 }
7 }
8}
9
10fragment ProductVariation on Product {
11 variation { #1
12 name #2
13 }
14}Note that the router calculates the recursion depth for each operation and fragment separately. Even if a fragment is included in an operation, that fragment's recursion depth does not contribute to the operation's recursion depth.
experimental_parser_recursion_limit.