Skip to main content

graphql-limit-count

The graphql-limit-count plugin uses fixed windows to limit the accumulated cost of GraphQL queries and mutations. Query depth is the default cost, preserving the plugin's original behavior. API7 Enterprise also provides complexity and node_quantifier strategies that account for the work a document requests.

In GraphQL, the depth refers to the number of nesting levels in a query or mutation. The following is an example query with a depth of 3:

{
a {
b {
c
}
}
}

With the default depth strategy, the plugin consumes a quota of depth within each time interval. For example, if the quota is 4 in a 30-second interval, a request with depth 3 is allowed and leaves 1. A request with depth 2 during the same interval is rejected.

The plugin accepts POST requests with either a JSON body containing a query field or an application/graphql body containing the GraphQL document. Fragments contribute to the calculated query depth. Unsupported methods return 405 Method Not Allowed; unreadable, malformed, or invalid GraphQL requests return 400 Bad Request.

APISIX reads up to 1 MiB of GraphQL request data by default. To change this limit, configure graphql.max_size in config.yaml and reload APISIX:

config.yaml
graphql:
max_size: 1048576

Local vs Redis Rate Limiting

The graphql-limit-count plugin supports two modes of rate limiting:

  • Local rate limiting: Limits are enforced independently on each gateway instance. Each instance maintains its own counters, so the effective limit is roughly (limit × number of instances) when traffic is spread across instances. This is the default when no policy is set or when policy is local.
  • Redis-based rate limiting: Limits are shared across all gateway instances through Redis. All instances share the same quota, so the configured limit applies to all gateway instances.

Query Cost

The complexity and node_quantifier strategies and their supporting fields were introduced in API7 Enterprise 3.10.6.

By default, a request is charged the depth of its query. cost_strategy selects a different cost model, so a request consumes quota in proportion to how much work it asks the upstream for:

  • depth charges the selection nesting depth. This is what the plugin has always done and remains the default, so an existing configuration keeps its behavior after an upgrade.
  • complexity computes the raw score from the nodes the query resolves. Each node contributes (sum of its children) × mul + add, where add and mul default to 1.
  • node_quantifier computes the raw score only from nodes whose matching cost decoration names a usable quantifier in mul_arguments. For example, a decoration with mul_arguments: ["first"] carries first: 10 as the multiplier for deeper quantified nodes. If no node has both a matching decoration and a usable quantifier, the document's raw score is 0. The default score_factor produces a charged cost of 1; a factor greater than 100 increases it after the 0.01 adjustment.

The plugin turns the raw strategy score into the integer charged against the quota. For complexity and node_quantifier, it adds 0.01 to the raw score before applying score_factor, then rounds the result up. With the default factor of 1, an integer raw score of 3 is therefore charged as 4. The depth strategy skips the 0.01 adjustment but still applies the factor and rounds up.

max_cost rejects a query whose charged cost exceeds the configured value with 403 Forbidden before it reaches the upstream. The plugin charges the quota before applying this check, so a rejected over-cost query still consumes its computed amount. When show_limit_quota_header is enabled, X-Graphql-Query-Cost reports that amount.

With resolve_variables enabled, which is the default, the plugin resolves supplied GraphQL variables, variable defaults declared by the operation, and argument defaults from the upstream schema before computing cost. Turning it off treats first: $n like an absent argument and can assign too little cost to a query whose quantifier is supplied through a variable.

Matching a cost decoration against the query requires the upstream schema. Each gateway worker introspects a Service with decorations on the first applicable request and caches the schema until the plugin reloads. A route with no decorations is never introspected. In that case, complexity counts each node with the default weight, while node_quantifier produces a raw score of 0; its charged cost follows the adjustment and scaling described above. Set introspection_endpoint when the introspection endpoint is not the upstream itself, and introspection_headers when it requires credentials. The credentials come from the configuration rather than from the request because each cached schema is reused by callers handled by that worker.

Cost Decorations

A decoration adjusts what one position in the upstream schema contributes to the cost. Decorations are managed on the Service as graphql_cost_decorations, so they are shared by every route under it and can be changed without editing the routes that carry the plugin.

A decoration names a field_path, which can identify a GraphQL type such as Product, a type and field such as Product.name, or a chain such as Query.products.nodes. It adjusts the node it matches:

FieldEffect
add_valueAdded to the node's own cost.
mul_valueMultiplies the cost of the node's children.
add_argumentsNames arguments whose values are added to the node's own cost.
mul_argumentsNames arguments whose values multiply descendant cost. Under node_quantifier, the multiplier carries to deeper quantified nodes.

A field_path can only be decorated once per Service.

Examples

The examples below use GitHub GraphQL API endpoint as an upstream and demonstrate how you can configure graphql-limit-count for different scenarios.

To follow along, create a GitHub personal access token with the appropriate scopes for the resources you want to interact with.

Apply Rate Limiting by Remote Address

The following example demonstrates the rate limiting of GraphQL requests by a single variable, remote_addr.

Create a route with graphql-limit-count plugin that allows for a quota of depth 2 within a 30-second window per remote address:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "graphql-limit-count-route",
"uri": "/graphql",
"plugins": {
"graphql-limit-count": {
"count": 2,
"time_window": 30,
"rejected_code": 429,
"key_type": "var",
"key": "remote_addr",
"policy": "local"
}
},
"upstream": {
"type": "roundrobin",
"pass_host": "node",
"scheme": "https",
"nodes": {
"api.github.com:443": 1
}
}
}'

Verify with GraphQL Query

Send a request with a GraphQL query of depth 2 to verify:

curl -i "http://127.0.0.1:9080/graphql" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${GH_ACCESS_TOKEN}" \
-d '{"query": "query {viewer{login}}"}'

You should see an HTTP/1.1 200 OK response with the corresponding response body.

The request has consumed all the quota allowed for the time window. If you send the request again within the same 30-second time interval, you should receive an HTTP/1.1 429 Too Many Requests response, indicating the request surpasses the quota threshold.

Verify with GraphQL Mutation

You can also send a request with a GraphQL mutation of depth 3 to verify:

curl -i "http://127.0.0.1:9080/graphql" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${GH_ACCESS_TOKEN}" \
-d '{"query": "mutation AddReactionToIssue {addReaction(input:{subjectId:\"MDU6SXNzdWUyMzEzOTE1NTE=\",content:HOORAY}) {reaction {content} subject {id}}}"}'

You should see an HTTP/1.1 429 Too Many Requests response at any time, as depth 3 always surpasses the quota of depth 2.

Apply Rate Limiting by Remote Address and Consumer Name

The following example demonstrates the rate limiting of GraphQL requests by a combination of variables, remote_addr and consumer_name. It allows for a quota of depth 2 within a 30-second window per remote address and for each consumer.

Create a consumer john:

curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "john"
}'

Create key-auth credential for the consumer:

curl "http://127.0.0.1:9180/apisix/admin/consumers/john/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-john-key-auth",
"plugins": {
"key-auth": {
"key": "john-key"
}
}
}'

Create a second consumer jane:

curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "jane"
}'

Create key-auth credential for the consumer:

curl "http://127.0.0.1:9180/apisix/admin/consumers/jane/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-jane-key-auth",
"plugins": {
"key-auth": {
"key": "jane-key"
}
}
}'

Create a route with key-auth and graphql-limit-count plugins:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "graphql-limit-count-route",
"uri": "/graphql",
"plugins": {
"key-auth": {},
"graphql-limit-count": {
"count": 2,
"time_window": 30,
"rejected_code": 429,
"policy": "local",
"key_type": "var_combination",
"key": "$remote_addr $consumer_name"
}
},
"upstream": {
"type": "roundrobin",
"pass_host": "node",
"scheme": "https",
"nodes": {
"api.github.com:443": 1
}
}
}'

key-auth: enable key authentication on the route.

key_type: set to var_combination to interpret the key as a combination of variables.

key: set to $remote_addr $consumer_name to apply rate limiting quota by remote address and consumer.

Send a request with a GraphQL query of depth 2 as the consumer jane:

curl -i "http://127.0.0.1:9080/graphql" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${GH_ACCESS_TOKEN}" \
-H 'apikey: jane-key' \
-d '{"query": "query {viewer{login}}"}'

You should see an HTTP/1.1 200 OK response with the corresponding response body.

This request has consumed all the quota set for the time window. If you send the same request as the consumer jane within the same 30-second time interval, you should receive an HTTP/1.1 429 Too Many Requests response, indicating the request surpasses the quota threshold.

Send the same request as the consumer john within the same 30-second time interval:

curl -i "http://127.0.0.1:9080/graphql" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${GH_ACCESS_TOKEN}" \
-H 'apikey: john-key' \
-d '{"query": "query {viewer{login}}"}'

You should see an HTTP/1.1 200 OK response with the corresponding response body, indicating the request is not rate limited.

Send the same request as the consumer john again within the same 30-second time interval, you should receive an HTTP/1.1 429 Too Many Requests response.

This verifies the plugin rate limits by the combination of variables, remote_addr and consumer_name.

Share Quota among Routes

The following example demonstrates the sharing of GraphQL rate limiting quota among multiple routes by configuring the group of the graphql-limit-count plugin.

Note that the configurations of the graphql-limit-count plugin of the same group should be identical. To avoid update anomalies and repetitive configurations, you can create a service with graphql-limit-count plugin and upstream for routes to connect to.

Create a service:

curl "http://127.0.0.1:9180/apisix/admin/services" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "graphql-limit-count-service",
"plugins": {
"graphql-limit-count": {
"count": 2,
"time_window": 30,
"rejected_code": 429,
"policy": "local",
"group": "srv1"
}
},
"upstream": {
"type": "roundrobin",
"pass_host": "node",
"scheme": "https",
"nodes": {
"api.github.com:443": 1
}
}
}'

Create two routes and configure their service_id to be graphql-limit-count-service, so that they share the same configurations for the plugin and upstream:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "graphql-limit-count-route-1",
"service_id": "graphql-limit-count-service",
"uri": "/graphql1",
"plugins": {
"proxy-rewrite": {
"uri": "/graphql"
}
}
}'
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "graphql-limit-count-route-2",
"service_id": "graphql-limit-count-service",
"uri": "/graphql2",
"plugins": {
"proxy-rewrite": {
"uri": "/graphql"
}
}
}'
note

The proxy-rewrite plugin is used to rewrite the URI to /graphql so that requests are forwarded to the correct endpoint.

Send a request with a GraphQL query of depth 2 to route /graphql1:

curl -i "http://127.0.0.1:9080/graphql1" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${GH_ACCESS_TOKEN}" \
-d '{"query": "query {viewer{login}}"}'

You should see an HTTP/1.1 200 OK response with the corresponding response body.

Send the same query of depth 2 to route /graphql2 within the same 30-second time interval:

curl -i "http://127.0.0.1:9080/graphql2" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${GH_ACCESS_TOKEN}" \
-d '{"query": "query {viewer{login}}"}'

You should receive an HTTP/1.1 429 Too Many Requests response, which verifies the two routes share the same rate limiting quota.

Share Quota Among Gateway Nodes with a Redis Server

The following example demonstrates the rate limiting of GraphQL requests across multiple gateway nodes with a Redis server, such that different gateway nodes share the same rate limiting quota.

Create a route with the following configurations in the gateway group:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "graphql-limit-count-route",
"uri": "/graphql",
"plugins": {
"graphql-limit-count": {
"count": 2,
"time_window": 30,
"rejected_code": 429,
"key": "remote_addr",
"policy": "redis",
"redis_host": "192.168.xxx.xxx",
"redis_port": 6379,
"redis_password": "p@ssw0rd",
"redis_database": 1
}
},
"upstream": {
"type": "roundrobin",
"pass_host": "node",
"scheme": "https",
"nodes": {
"api.github.com:443": 1
}
}
}'

policy: set to redis to use a Redis instance for rate limiting.

redis_host: set to Redis instance IP address.

redis_port: set to Redis instance listening port.

redis_password: set to the password of the Redis instance, if any.

redis_database: set to the database number in the Redis instance.

Send a request with a GraphQL query of depth 2 to a gateway instance:

curl -i "http://127.0.0.1:9080/graphql" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${GH_ACCESS_TOKEN}" \
-d '{"query": "query {viewer{login}}"}'

You should see an HTTP/1.1 200 OK response with the corresponding response body.

Send the same request to a different gateway instance within the same 30-second time interval, you should receive an HTTP/1.1 429 Too Many Requests response, verifying routes configured in different gateway nodes share the same quota.

Share Quota Among Gateway Nodes with a Redis Cluster

You can also use a Redis cluster to apply the same quota across multiple gateway nodes, such that different gateway nodes share the same rate limiting quota.

Ensure that your Redis instances are running in cluster mode. A minimum of two nodes are required for the graphql-limit-count plugin configurations.

Create a route with the following configurations in the gateway group:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "graphql-limit-count-route",
"uri": "/graphql",
"plugins": {
"graphql-limit-count": {
"count": 2,
"time_window": 30,
"rejected_code": 429,
"key": "remote_addr",
"policy": "redis-cluster",
"redis_cluster_nodes": [
"192.168.xxx.xxx:6379",
"192.168.xxx.xxx:16379"
],
"redis_password": "p@ssw0rd",
"redis_cluster_name": "redis-cluster-1",
"redis_cluster_ssl": true
}
},
"upstream": {
"type": "roundrobin",
"pass_host": "node",
"scheme": "https",
"nodes": {
"api.github.com:443": 1
}
}
}'

policy: set to redis-cluster to use a Redis cluster for rate limiting.

redis_cluster_nodes: set to Redis node addresses in the Redis cluster.

redis_password: set to the password of the Redis cluster, if any.

redis_cluster_name: set to the Redis cluster name.

redis_cluster_ssl: enable SSL/TLS communication with Redis cluster.

Send a request with a GraphQL query of depth 2 to a gateway instance:

curl -i "http://127.0.0.1:9080/graphql" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${GH_ACCESS_TOKEN}" \
-d '{"query": "query {viewer{login}}"}'

You should see an HTTP/1.1 200 OK response with the corresponding response body.

Send the same request to a different gateway instance within the same 30-second time interval, you should receive an HTTP/1.1 429 Too Many Requests response, verifying routes configured in different gateway nodes share the same quota.

Rate Limit by Query Complexity

The following example calculates the raw score from the nodes a query resolves instead of its depth, then rejects a query whose charged cost exceeds a fixed budget. This example applies to API7 Enterprise 3.10.6 and later.

Create a route with the graphql-limit-count plugin that charges the complexity cost. It allows a quota of 100 within a 30-second window per remote address, and rejects any single query costing more than 20:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "graphql-cost-route",
"uri": "/graphql",
"plugins": {
"graphql-limit-count": {
"count": 100,
"time_window": 30,
"rejected_code": 429,
"key_type": "var",
"key": "remote_addr",
"policy": "local",
"show_limit_quota_header": true,
"cost_strategy": "complexity",
"max_cost": 20
}
},
"upstream": {
"type": "roundrobin",
"pass_host": "node",
"scheme": "https",
"nodes": {
"api.github.com:443": 1
}
}
}'

Send a small query to a gateway instance:

curl -i "http://127.0.0.1:9080/graphql" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${GH_ACCESS_TOKEN}" \
-d '{"query": "query {viewer{login}}"}'

You should see an HTTP/1.1 200 OK response carrying the cost it was charged:

X-Graphql-Query-Cost: 4

Send a query whose charged cost exceeds the budget:

curl -i "http://127.0.0.1:9080/graphql" -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${GH_ACCESS_TOKEN}" \
-d '{"query": "query {viewer{login name email location company bio websiteUrl twitterUsername createdAt updatedAt databaseId url avatarUrl isHireable isViewer isEmployee isSiteAdmin pronouns}}"}'

You should see an HTTP/1.1 403 Forbidden response, and the query never reaches the upstream:

{"message":"Invalid graphql request: query cost 21 exceeds max_cost 20"}