Skip to main content

graphql-limit-count

The graphql-limit-count plugin uses fixed or sliding windows to limit the accumulated cost of GraphQL queries and mutations. Query depth is the default cost. The complexity and node_quantifier strategies can instead 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.

See GraphQL Query Cost Reference for advanced cost strategies, schema introspection, and service-level cost decorations.

Local vs Redis Rate Limiting​

The graphql-limit-count plugin supports local and Redis-backed 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-backed rate limiting: Limits are shared across gateway instances through a Redis server, Redis Cluster, or Redis Sentinel deployment. All instances share the same quota.

Examples​

The examples use the public Countries GraphQL API as the upstream. It does not require an account or access token.

For the Ingress Controller tabs, complete Set Up Ingress Controller and Gateway first. The Gateway API examples use the APISIX setup's Gateway named apisix in namespace aic; replace that name with your installed Gateway when using another deployment, including API7 Gateway.

Apply Rate Limiting by Remote Address​

The following example demonstrates how to give each client address an independent query-cost quota.

Create a route that allows a total query depth of 2 within each 30-second window per remote address:

curl "http://127.0.0.1:9180/apisix/admin/routes/graphql-limit-count-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"uri": "/graphql-limit",
"plugins": {
"proxy-rewrite": {
"uri": "/graphql"
},
"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": {
"countries.trevorblades.com:443": 1
}
}
}'

Verify a GraphQL Query​

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

curl -i "http://127.0.0.1:9080/graphql-limit" -X POST \
-H "Content-Type: application/json" \
-d '{"query": "query { country(code: \"US\") { name } }"}'

The first request should return 200 OK with X-RateLimit-Remaining: 0 and the United States in the response body.

The request consumes the available quota. Repeat it within the same 30-second window. The second request should return 429 Too Many Requests.

Verify a GraphQL Mutation​

The Countries API is read-only, but the plugin evaluates a mutation before the request reaches the upstream. Send a syntactically valid mutation with a depth of 3:

curl -i "http://127.0.0.1:9080/graphql-limit" -X POST \
-H "Content-Type: application/json" \
-d '{"query":"mutation { updateCountry { country { name } } }"}'

The request should return 429 Too Many Requests because its depth exceeds the configured quota. The request is rejected before the read-only upstream validates the operation.

Rate Limit by Query Complexity​

The complexity strategy and max_cost were introduced in API7 Enterprise 3.10.6 and APISIX 3.19.0.

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.

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

curl "http://127.0.0.1:9180/apisix/admin/routes/graphql-cost-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"uri": "/graphql-complexity-limit",
"plugins": {
"proxy-rewrite": {
"uri": "/graphql"
},
"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": 6
}
},
"upstream": {
"type": "roundrobin",
"pass_host": "node",
"scheme": "https",
"nodes": {
"countries.trevorblades.com:443": 1
}
}
}'

Send a small query to a gateway instance:

curl -i "http://127.0.0.1:9080/graphql-complexity-limit" -X POST \
-H "Content-Type: application/json" \
-d '{"query": "query { country(code: \"US\") { name } }"}'

The response should return 200 OK and report the charged cost:

X-Graphql-Query-Cost: 4

Send a query whose charged cost exceeds the budget:

curl -i "http://127.0.0.1:9080/graphql-complexity-limit" -X POST \
-H "Content-Type: application/json" \
-d '{"query": "query { country(code: \"US\") { code name native phone capital currency emoji continent { name } languages { name } } }"}'

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

{"message":"Invalid graphql request: query cost 14 exceeds max_cost 6"}

Apply Rate Limiting by Remote Address and Consumer Name​

The following example demonstrates how to combine the client address and consumer name so each authenticated consumer receives an independent quota from the same address. Each key combination can use a total query depth of 2 within a 30-second window.

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/graphql-limit-count-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"uri": "/graphql-consumer-limit",
"plugins": {
"key-auth": {},
"proxy-rewrite": {
"uri": "/graphql"
},
"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": {
"countries.trevorblades.com:443": 1
}
}
}'

❶ key-auth enables key authentication on the route.

❷ key_type: var_combination interprets key as a combination of variables.

❸ key combines the client address and consumer name so each resolved pair has an independent quota.

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

curl -i "http://127.0.0.1:9080/graphql-consumer-limit" -X POST \
-H "Content-Type: application/json" \
-H 'apikey: jane-key' \
-d '{"query": "query { country(code: \"US\") { name } }"}'

The request should return 200 OK with X-RateLimit-Remaining: 0 and the United States in the response body.

This request consumes Jane's available quota. Repeating it as Jane within the same 30-second window should return 429 Too Many Requests.

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

curl -i "http://127.0.0.1:9080/graphql-consumer-limit" -X POST \
-H "Content-Type: application/json" \
-H 'apikey: john-key' \
-d '{"query": "query { country(code: \"US\") { name } }"}'

John's first request should also return 200 OK with X-RateLimit-Remaining: 0, confirming that it uses a different counter from Jane's request.

Repeat the request as John within the same window. The second request should return 429 Too Many Requests.

These results confirm that the plugin maintains a separate counter for each resolved address-and-consumer key.

Share Quota among Routes​

The following example demonstrates how to assign two routes to the same plugin group so they consume one shared query-cost quota.

Routes that share a group must use identical graphql-limit-count configurations. To keep the configuration consistent, attach the plugin and upstream to a service, then associate the routes with that service.

Create a service:

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

Create the first route and attach it to the service:

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

Create the second route and attach it to the same service:

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

Both routes use proxy-rewrite to send their public paths to the upstream /graphql 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" \
-d '{"query": "query { country(code: \"US\") { name } }"}'

The first route should return 200 OK with X-RateLimit-Remaining: 0.

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" \
-d '{"query": "query { country(code: \"US\") { name } }"}'

The second route should return 429 Too Many Requests, confirming that both routes consume the same group quota.

Share Quota among Gateway Instances with a Redis Server​

The following example demonstrates how to store query-cost counters in one Redis server so gateway nodes that load the same route resource enforce one shared quota.

Start Redis​

Start Redis in the environment used by the gateway.

Set GATEWAY_CONTAINER to the running APISIX or API7 Gateway container. Create a dedicated network and connect the gateway to it:

export GATEWAY_CONTAINER=replace-with-gateway-container-name

docker network create gateway-graphql-limit-redis-net
docker network connect gateway-graphql-limit-redis-net "$GATEWAY_CONTAINER"

Repeat the network connection for every additional gateway container used to verify the shared counter:

docker network connect gateway-graphql-limit-redis-net replace-with-additional-gateway-container

Start Redis on the shared network:

docker run -d \
--name redis-standalone \
--network gateway-graphql-limit-redis-net \
redis:8.10.2-alpine \
redis-server --requirepass redis-password

Wait until Redis is ready, then verify the authenticated connection:

docker exec -e REDISCLI_AUTH=redis-password redis-standalone redis-cli ping

You should receive PONG.

These fixtures use a tutorial password and internal service address. For production, restrict network access, manage credentials as secrets, enable persistence, and configure TLS where supported.

Create a Route​

Create the route in the gateway cluster or gateway group:

curl "http://127.0.0.1:9180/apisix/admin/routes/graphql-limit-count-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"uri": "/graphql-redis-limit",
"plugins": {
"proxy-rewrite": {
"uri": "/graphql"
},
"graphql-limit-count": {
"count": 2,
"time_window": 30,
"rejected_code": 429,
"key": "remote_addr",
"policy": "redis",
"redis_host": "redis-standalone",
"redis_port": 6379,
"redis_password": "redis-password",
"redis_database": 0
}
},
"upstream": {
"type": "roundrobin",
"pass_host": "node",
"scheme": "https",
"nodes": {
"countries.trevorblades.com:443": 1
}
}
}'

❶ policy: redis stores rate limiting counters in one Redis server.

❷ redis_host identifies the Redis service started above.

❸ redis_port specifies the Redis listening port.

❹ redis_password authenticates with the tutorial Redis password.

❺ redis_database stores counters in Redis database 0.

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

curl -i "http://127.0.0.1:9080/graphql-redis-limit" -X POST \
-H "Content-Type: application/json" \
-d '{"query": "query { country(code: \"US\") { name } }"}'

The request should return 200 OK with X-RateLimit-Remaining: 0.

Send the same request to another gateway instance within the same 30-second window. It should return 429 Too Many Requests, confirming that the gateway nodes share the quota.

Share Quota among Gateway Instances with a Redis Cluster​

The following example demonstrates how to store query-cost counters in a Redis Cluster so gateway nodes share a partitioned, replicated counter store.

Start Redis Cluster​

Set GATEWAY_CONTAINER to the running APISIX or API7 Gateway container. Create a dedicated network and connect the gateway to it:

export GATEWAY_CONTAINER=replace-with-gateway-container-name

docker network create gateway-graphql-limit-cluster-net
docker network connect gateway-graphql-limit-cluster-net "$GATEWAY_CONTAINER"

Repeat the network connection for every additional gateway container used to verify the shared counter:

docker network connect gateway-graphql-limit-cluster-net replace-with-additional-gateway-container

Create the following Docker Compose file for three primary nodes and three replicas:

docker-compose.yml
x-redis-node: &redis-node
image: redis:8.10.2-alpine
command:
- redis-server
- --cluster-enabled
- "yes"
- --cluster-config-file
- nodes.conf
- --cluster-node-timeout
- "5000"
- --appendonly
- "no"
- --requirepass
- redis-cluster-password
- --masterauth
- redis-cluster-password
healthcheck:
test: ["CMD", "redis-cli", "-a", "redis-cluster-password", "ping"]
interval: 2s
timeout: 2s
retries: 30
networks:
- gateway-graphql-limit-cluster-net

services:
graphql-redis-cluster-1:
<<: *redis-node
container_name: graphql-redis-cluster-1
graphql-redis-cluster-2:
<<: *redis-node
container_name: graphql-redis-cluster-2
graphql-redis-cluster-3:
<<: *redis-node
container_name: graphql-redis-cluster-3
graphql-redis-cluster-4:
<<: *redis-node
container_name: graphql-redis-cluster-4
graphql-redis-cluster-5:
<<: *redis-node
container_name: graphql-redis-cluster-5
graphql-redis-cluster-6:
<<: *redis-node
container_name: graphql-redis-cluster-6

networks:
gateway-graphql-limit-cluster-net:
external: true

Start the nodes and wait for their health checks:

docker compose up -d --wait

Initialize the cluster with one replica for each primary:

docker exec -e REDISCLI_AUTH=redis-cluster-password graphql-redis-cluster-1 \
redis-cli --cluster create \
graphql-redis-cluster-1:6379 \
graphql-redis-cluster-2:6379 \
graphql-redis-cluster-3:6379 \
graphql-redis-cluster-4:6379 \
graphql-redis-cluster-5:6379 \
graphql-redis-cluster-6:6379 \
--cluster-replicas 1 \
--cluster-yes

The output should end with [OK] All 16384 slots covered.

Wait a few seconds for the nodes to exchange cluster state, then verify that all hash slots are available:

docker exec -e REDISCLI_AUTH=redis-cluster-password graphql-redis-cluster-1 \
redis-cli cluster info

The output should include cluster_state:ok and cluster_slots_assigned:16384.

Export the Redis node addresses for the gateway configuration:

export REDIS_CLUSTER_NODE_1="graphql-redis-cluster-1:6379"
export REDIS_CLUSTER_NODE_2="graphql-redis-cluster-2:6379"
export REDIS_CLUSTER_NODE_3="graphql-redis-cluster-3:6379"
export REDIS_CLUSTER_NODE_4="graphql-redis-cluster-4:6379"
export REDIS_CLUSTER_NODE_5="graphql-redis-cluster-5:6379"
export REDIS_CLUSTER_NODE_6="graphql-redis-cluster-6:6379"

These fixtures use a tutorial password and internal service addresses. For production, restrict network access, manage credentials as secrets, enable persistence, and configure TLS.

Create a Route​

Create the route in the gateway cluster or gateway group:

curl "http://127.0.0.1:9180/apisix/admin/routes/graphql-limit-count-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d @- <<EOF
{
"uri": "/graphql-cluster-limit",
"plugins": {
"proxy-rewrite": {
"uri": "/graphql"
},
"graphql-limit-count": {
"count": 2,
"time_window": 30,
"rejected_code": 429,
"key": "remote_addr",
"policy": "redis-cluster",
"redis_cluster_nodes": [
"${REDIS_CLUSTER_NODE_1}",
"${REDIS_CLUSTER_NODE_2}",
"${REDIS_CLUSTER_NODE_3}",
"${REDIS_CLUSTER_NODE_4}",
"${REDIS_CLUSTER_NODE_5}",
"${REDIS_CLUSTER_NODE_6}"
],
"redis_password": "redis-cluster-password",
"redis_cluster_name": "graphql-limit-docs-redis-cluster"
}
},
"upstream": {
"type": "roundrobin",
"pass_host": "node",
"scheme": "https",
"nodes": {
"countries.trevorblades.com:443": 1
}
}
}
EOF

❶ policy: redis-cluster stores rate limiting counters in Redis Cluster.

❷ redis_cluster_nodes lists the Redis Cluster nodes started above. The Admin API and ADC examples use the addresses exported for the selected environment.

❸ redis_password authenticates with the tutorial Redis Cluster password.

❹ redis_cluster_name identifies this Redis Cluster configuration so the gateway does not reuse another cluster's cached slot map.

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

curl -i "http://127.0.0.1:9080/graphql-cluster-limit" -X POST \
-H "Content-Type: application/json" \
-d '{"query": "query { country(code: \"US\") { name } }"}'

The request should return 200 OK with X-RateLimit-Remaining: 0.

Send the same request to another gateway instance within the same 30-second window. It should return 429 Too Many Requests, confirming that the gateway nodes share the quota.