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

YAML
router.yaml
1limits:
2  # Network-based limits
3  http_max_request_bytes: 2000000 # Default value: 2 MB
4
5  # Parser-based limits
6  parser_max_tokens: 15000 # Default value
7  parser_max_recursion: 500 # Default value
8
9  # Operation-based limits (Enterprise only)
10  max_depth: 100
11  max_height: 200
12  max_aliases: 30
13  max_root_fields: 20

Operation-based limits

This feature is only available with a GraphOS Enterprise plan. You can test it out by signing up for a free GraphOS trial. To compare GraphOS feature support across all plan types, see the pricing page.

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:

YAML
router.yaml
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: true

Each 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:

GraphQL
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:

GraphQL
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:

GraphQL
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:

GraphQL
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:

Text
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:

YAML
router.yaml
1limits:
2  warn_only: true # warn_only mode always enabled

Response 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:

json5
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:

YAML
router.yaml
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_fields
 note
For a large amount of traffic, you may prefer to collect and export metrics to your APM instead.

Collecting 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:

YAML
router.yaml
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.

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.

GraphQL
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.

 note
In versions of the Apollo Router prior to 1.17, this limit was defined via the config option experimental_parser_recursion_limit.
Feedback

Edit on GitHub

Forums