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
policyis set or whenpolicyislocal. - 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:
- Admin API
- ADC
- Ingress Controller
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
}
}
}'
services:
- name: graphql-service
labels:
docs-example: graphql-limit-remote
routes:
- uris:
- /graphql-limit
name: graphql-limit-count-route
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
scheme: https
pass_host: node
nodes:
- host: countries.trevorblades.com
port: 443
weight: 1
Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=graphql-limit-remote
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=graphql-limit-remote
- Gateway API
- APISIX CRD
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: countries-graphql-external-domain
spec:
type: ExternalName
externalName: countries.trevorblades.com
ports:
- name: https
port: 443
targetPort: 443
---
apiVersion: apisix.apache.org/v1alpha1
kind: BackendTrafficPolicy
metadata:
namespace: aic
name: countries-graphql-https
spec:
targetRefs:
- name: countries-graphql-external-domain
kind: Service
group: ""
sectionName: https
passHost: node
scheme: https
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: graphql-limit-count-plugin-config
spec:
plugins:
- name: proxy-rewrite
config:
uri: /graphql
- name: graphql-limit-count
config:
count: 2
time_window: 30
rejected_code: 429
key_type: var
key: remote_addr
policy: local
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: graphql-limit-count-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /graphql-limit
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: graphql-limit-count-plugin-config
backendRefs:
- name: countries-graphql-external-domain
port: 443
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: countries-graphql-external-domain
spec:
ingressClassName: apisix
scheme: https
passHost: node
externalNodes:
- type: Domain
name: countries.trevorblades.com
port: 443
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: graphql-limit-count-route
spec:
ingressClassName: apisix
http:
- name: graphql-limit-count-route
match:
paths:
- /graphql-limit
upstreams:
- name: countries-graphql-external-domain
plugins:
- name: proxy-rewrite
enable: true
config:
uri: /graphql
- name: graphql-limit-count
enable: true
config:
count: 2
time_window: 30
rejected_code: 429
key_type: var
key: remote_addr
policy: local
Apply the configuration to your cluster:
kubectl apply -f graphql-limit-count-ic.yaml
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:
- Admin API
- ADC
- Ingress Controller
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
}
}
}'
services:
- name: graphql-service
labels:
docs-example: graphql-limit-complexity
routes:
- uris:
- /graphql-complexity-limit
name: graphql-cost-route
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
scheme: https
pass_host: node
nodes:
- host: countries.trevorblades.com
port: 443
weight: 1
Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=graphql-limit-complexity
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=graphql-limit-complexity
- Gateway API
- APISIX CRD
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: countries-graphql-external-domain
spec:
type: ExternalName
externalName: countries.trevorblades.com
ports:
- name: https
port: 443
targetPort: 443
---
apiVersion: apisix.apache.org/v1alpha1
kind: BackendTrafficPolicy
metadata:
namespace: aic
name: countries-graphql-https
spec:
targetRefs:
- name: countries-graphql-external-domain
kind: Service
group: ""
sectionName: https
passHost: node
scheme: https
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: graphql-cost-plugin-config
spec:
plugins:
- name: proxy-rewrite
config:
uri: /graphql
- name: graphql-limit-count
config:
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
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: graphql-cost-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /graphql-complexity-limit
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: graphql-cost-plugin-config
backendRefs:
- name: countries-graphql-external-domain
port: 443
Apply the configuration to your cluster:
kubectl apply -f graphql-cost-ic.yaml
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: countries-graphql-external-domain
spec:
type: ExternalName
externalName: countries.trevorblades.com
ports:
- name: https
port: 443
targetPort: 443
---
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: countries-graphql-external-domain
spec:
ingressClassName: apisix
passHost: node
scheme: https
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: graphql-cost-route
spec:
ingressClassName: apisix
http:
- name: graphql-cost-route
match:
paths:
- /graphql-complexity-limit
backends:
- serviceName: countries-graphql-external-domain
servicePort: 443
plugins:
- name: proxy-rewrite
enable: true
config:
uri: /graphql
- name: graphql-limit-count
enable: true
config:
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
Apply the configuration to your cluster:
kubectl apply -f graphql-cost-ic.yaml
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.
- Admin API
- ADC
- Ingress Controller
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
}
}
}'
Create two consumers and a route that enables rate limiting by consumers:
consumers:
- username: john
labels:
docs-example: graphql-limit-consumer
credentials:
- name: key-auth
type: key-auth
config:
key: john-key
- username: jane
labels:
docs-example: graphql-limit-consumer
credentials:
- name: key-auth
type: key-auth
config:
key: jane-key
services:
- name: graphql-limit-service
labels:
docs-example: graphql-limit-consumer
routes:
- name: graphql-limit-count-route
uris:
- /graphql-consumer-limit
plugins:
proxy-rewrite:
uri: /graphql
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
scheme: https
pass_host: node
nodes:
- host: countries.trevorblades.com
port: 443
weight: 1
Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--include-resource-type consumer \
--label-selector docs-example=graphql-limit-consumer
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--include-resource-type consumer \
--label-selector docs-example=graphql-limit-consumer
Create two consumers and a route that enables rate limiting by consumers:
- Gateway API
- APISIX CRD
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: john
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: primary-key
config:
key: john-key
---
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: jane
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: primary-key
config:
key: jane-key
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: countries-graphql-external-domain
spec:
type: ExternalName
externalName: countries.trevorblades.com
ports:
- name: https
port: 443
targetPort: 443
---
apiVersion: apisix.apache.org/v1alpha1
kind: BackendTrafficPolicy
metadata:
namespace: aic
name: countries-graphql-https
spec:
targetRefs:
- name: countries-graphql-external-domain
kind: Service
group: ""
sectionName: https
passHost: node
scheme: https
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: graphql-limit-count-plugin-config
spec:
plugins:
- name: proxy-rewrite
config:
uri: /graphql
- name: key-auth
config:
_meta:
disable: false
- name: graphql-limit-count
config:
count: 2
time_window: 30
rejected_code: 429
policy: local
key_type: var_combination
key: "$remote_addr $consumer_name"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: graphql-limit-count-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /graphql-consumer-limit
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: graphql-limit-count-plugin-config
backendRefs:
- name: countries-graphql-external-domain
port: 443
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: john
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: john-key
---
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: jane
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: jane-key
---
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: countries-graphql-external-domain
spec:
ingressClassName: apisix
scheme: https
passHost: node
externalNodes:
- type: Domain
name: countries.trevorblades.com
port: 443
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: graphql-limit-count-route
spec:
ingressClassName: apisix
http:
- name: graphql-limit-count-route
match:
paths:
- /graphql-consumer-limit
upstreams:
- name: countries-graphql-external-domain
plugins:
- name: proxy-rewrite
enable: true
config:
uri: /graphql
- name: key-auth
enable: true
- name: graphql-limit-count
enable: true
config:
count: 2
time_window: 30
rejected_code: 429
policy: local
key_type: var_combination
key: "$remote_addr $consumer_name"
Apply the configuration to your cluster:
kubectl apply -f graphql-limit-count-ic.yaml
❶ 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.
- Admin API
- ADC
- Ingress Controller
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"
}
}
}'
Create a service with two routes that share the same rate limiting quota:
services:
- name: graphql-limit-count-service
labels:
docs-example: graphql-limit-group
plugins:
graphql-limit-count:
count: 2
time_window: 30
rejected_code: 429
policy: local
group: graphql-srv1
routes:
- name: graphql-limit-count-route-1
uris:
- /graphql1
plugins:
proxy-rewrite:
uri: /graphql
- name: graphql-limit-count-route-2
uris:
- /graphql2
plugins:
proxy-rewrite:
uri: /graphql
upstream:
type: roundrobin
scheme: https
pass_host: node
nodes:
- host: countries.trevorblades.com
port: 443
weight: 1
Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=graphql-limit-group
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=graphql-limit-group
- Gateway API
- APISIX CRD
Create two HTTPRoute resources that reference the same PluginConfig to share quota:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: countries-graphql-external-domain
spec:
type: ExternalName
externalName: countries.trevorblades.com
ports:
- name: https
port: 443
targetPort: 443
---
apiVersion: apisix.apache.org/v1alpha1
kind: BackendTrafficPolicy
metadata:
namespace: aic
name: countries-graphql-https
spec:
targetRefs:
- name: countries-graphql-external-domain
kind: Service
group: ""
sectionName: https
passHost: node
scheme: https
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: graphql-limit-count-plugin-config
spec:
plugins:
- name: graphql-limit-count
config:
count: 2
time_window: 30
rejected_code: 429
policy: local
group: graphql-srv1
- name: proxy-rewrite
config:
uri: /graphql
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: graphql-limit-count-route-1
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /graphql1
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: graphql-limit-count-plugin-config
backendRefs:
- name: countries-graphql-external-domain
port: 443
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: graphql-limit-count-route-2
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /graphql2
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: graphql-limit-count-plugin-config
backendRefs:
- name: countries-graphql-external-domain
port: 443
Create an ApisixRoute with multiple paths that share the same plugin configuration:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: countries-graphql-external-domain
spec:
ingressClassName: apisix
scheme: https
passHost: node
externalNodes:
- type: Domain
name: countries.trevorblades.com
port: 443
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: graphql-limit-count-shared-route
spec:
ingressClassName: apisix
http:
- name: graphql-limit-count-shared
match:
paths:
- /graphql1
- /graphql2
upstreams:
- name: countries-graphql-external-domain
plugins:
- name: proxy-rewrite
enable: true
config:
uri: /graphql
- name: graphql-limit-count
enable: true
config:
count: 2
time_window: 30
rejected_code: 429
policy: local
group: graphql-srv1
Apply the configuration to your cluster:
kubectl apply -f graphql-limit-count-ic.yaml
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.
- Docker
- Kubernetes
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.
Create a Redis Deployment and Service in the gateway namespace:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: redis-standalone
spec:
replicas: 1
selector:
matchLabels:
app: redis-standalone
template:
metadata:
labels:
app: redis-standalone
spec:
containers:
- name: redis
image: redis:8.10.2-alpine
args:
- redis-server
- --requirepass
- redis-password
ports:
- containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: redis-standalone
spec:
selector:
app: redis-standalone
ports:
- port: 6379
targetPort: 6379
Apply the manifest and wait for Redis to be ready:
kubectl apply -f redis-standalone.yaml
kubectl rollout status deployment/redis-standalone -n aic
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
- Admin API
- ADC
- Ingress Controller
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
}
}
}'
Create a route with Redis-based rate limiting:
services:
- name: graphql-redis-limit-service
labels:
docs-example: graphql-limit-redis
routes:
- name: graphql-redis-limit-route
uris:
- /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
scheme: https
pass_host: node
nodes:
- host: countries.trevorblades.com
port: 443
weight: 1
Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=graphql-limit-redis
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=graphql-limit-redis
- Gateway API
- APISIX CRD
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: countries-graphql-external-domain
spec:
type: ExternalName
externalName: countries.trevorblades.com
ports:
- name: https
port: 443
targetPort: 443
---
apiVersion: apisix.apache.org/v1alpha1
kind: BackendTrafficPolicy
metadata:
namespace: aic
name: countries-graphql-https
spec:
targetRefs:
- name: countries-graphql-external-domain
kind: Service
group: ""
sectionName: https
passHost: node
scheme: https
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: graphql-limit-count-redis-plugin-config
spec:
plugins:
- name: proxy-rewrite
config:
uri: /graphql
- name: graphql-limit-count
config:
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
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: graphql-redis-limit-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /graphql-redis-limit
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: graphql-limit-count-redis-plugin-config
backendRefs:
- name: countries-graphql-external-domain
port: 443
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: countries-graphql-external-domain
spec:
ingressClassName: apisix
scheme: https
passHost: node
externalNodes:
- type: Domain
name: countries.trevorblades.com
port: 443
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: graphql-redis-limit-route
spec:
ingressClassName: apisix
http:
- name: graphql-redis-limit-route
match:
paths:
- /graphql-redis-limit
upstreams:
- name: countries-graphql-external-domain
plugins:
- name: proxy-rewrite
enable: true
config:
uri: /graphql
- name: graphql-limit-count
enable: true
config:
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
Apply the configuration to your cluster:
kubectl apply -f graphql-limit-count-ic.yaml
❶ 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
- Docker
- Kubernetes
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:
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"
Create a six-node Redis Cluster StatefulSet and headless Service:
apiVersion: apps/v1
kind: StatefulSet
metadata:
namespace: aic
name: graphql-redis-cluster
spec:
serviceName: graphql-redis-cluster
replicas: 6
selector:
matchLabels:
app: graphql-redis-cluster
template:
metadata:
labels:
app: graphql-redis-cluster
spec:
containers:
- name: redis
image: redis:8.10.2-alpine
ports:
- containerPort: 6379
name: client
- containerPort: 16379
name: gossip
command:
- redis-server
- --cluster-enabled
- "yes"
- --cluster-config-file
- nodes.conf
- --cluster-node-timeout
- "5000"
- --appendonly
- "yes"
- --requirepass
- redis-cluster-password
- --masterauth
- redis-cluster-password
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: graphql-redis-cluster
spec:
clusterIP: None
selector:
app: graphql-redis-cluster
ports:
- port: 6379
name: client
- port: 16379
name: gossip
Apply the manifest and wait for all nodes to be ready:
kubectl apply -f redis-cluster.yaml
kubectl rollout status statefulset/graphql-redis-cluster -n aic
Initialize the cluster with one replica for each primary:
kubectl exec -n aic graphql-redis-cluster-0 \
-- env REDISCLI_AUTH=redis-cluster-password redis-cli \
--cluster create \
graphql-redis-cluster-0.graphql-redis-cluster.aic.svc:6379 \
graphql-redis-cluster-1.graphql-redis-cluster.aic.svc:6379 \
graphql-redis-cluster-2.graphql-redis-cluster.aic.svc:6379 \
graphql-redis-cluster-3.graphql-redis-cluster.aic.svc:6379 \
graphql-redis-cluster-4.graphql-redis-cluster.aic.svc:6379 \
graphql-redis-cluster-5.graphql-redis-cluster.aic.svc:6379 \
--cluster-replicas 1 \
--cluster-yes
Verify that the cluster is ready:
kubectl exec -n aic graphql-redis-cluster-0 \
-- env REDISCLI_AUTH=redis-cluster-password 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-0.graphql-redis-cluster.aic.svc:6379"
export REDIS_CLUSTER_NODE_2="graphql-redis-cluster-1.graphql-redis-cluster.aic.svc:6379"
export REDIS_CLUSTER_NODE_3="graphql-redis-cluster-2.graphql-redis-cluster.aic.svc:6379"
export REDIS_CLUSTER_NODE_4="graphql-redis-cluster-3.graphql-redis-cluster.aic.svc:6379"
export REDIS_CLUSTER_NODE_5="graphql-redis-cluster-4.graphql-redis-cluster.aic.svc:6379"
export REDIS_CLUSTER_NODE_6="graphql-redis-cluster-5.graphql-redis-cluster.aic.svc: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
- Admin API
- ADC
- Ingress Controller
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
Create a route with Redis cluster-based rate limiting:
services:
- name: graphql-redis-cluster-limit-service
labels:
docs-example: graphql-limit-cluster
routes:
- name: graphql-redis-cluster-limit-route
uris:
- /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
scheme: https
pass_host: node
nodes:
- host: countries.trevorblades.com
port: 443
weight: 1
Render the exported Redis node addresses into the ADC file:
envsubst < adc.template.yaml > adc.yaml
Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=graphql-limit-cluster
Synchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=graphql-limit-cluster
- Gateway API
- APISIX CRD
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: countries-graphql-external-domain
spec:
type: ExternalName
externalName: countries.trevorblades.com
ports:
- name: https
port: 443
targetPort: 443
---
apiVersion: apisix.apache.org/v1alpha1
kind: BackendTrafficPolicy
metadata:
namespace: aic
name: countries-graphql-https
spec:
targetRefs:
- name: countries-graphql-external-domain
kind: Service
group: ""
sectionName: https
passHost: node
scheme: https
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: graphql-limit-count-redis-cluster-plugin-config
spec:
plugins:
- name: proxy-rewrite
config:
uri: /graphql
- name: graphql-limit-count
config:
count: 2
time_window: 30
rejected_code: 429
key: remote_addr
policy: redis-cluster
redis_cluster_nodes:
- graphql-redis-cluster-0.graphql-redis-cluster.aic.svc:6379
- graphql-redis-cluster-1.graphql-redis-cluster.aic.svc:6379
- graphql-redis-cluster-2.graphql-redis-cluster.aic.svc:6379
- graphql-redis-cluster-3.graphql-redis-cluster.aic.svc:6379
- graphql-redis-cluster-4.graphql-redis-cluster.aic.svc:6379
- graphql-redis-cluster-5.graphql-redis-cluster.aic.svc:6379
redis_password: redis-cluster-password
redis_cluster_name: graphql-limit-docs-redis-cluster
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: graphql-redis-cluster-limit-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /graphql-cluster-limit
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: graphql-limit-count-redis-cluster-plugin-config
backendRefs:
- name: countries-graphql-external-domain
port: 443
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: countries-graphql-external-domain
spec:
ingressClassName: apisix
scheme: https
passHost: node
externalNodes:
- type: Domain
name: countries.trevorblades.com
port: 443
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: graphql-redis-cluster-limit-route
spec:
ingressClassName: apisix
http:
- name: graphql-redis-cluster-limit-route
match:
paths:
- /graphql-cluster-limit
upstreams:
- name: countries-graphql-external-domain
plugins:
- name: proxy-rewrite
enable: true
config:
uri: /graphql
- name: graphql-limit-count
enable: true
config:
count: 2
time_window: 30
rejected_code: 429
key: remote_addr
policy: redis-cluster
redis_cluster_nodes:
- graphql-redis-cluster-0.graphql-redis-cluster.aic.svc:6379
- graphql-redis-cluster-1.graphql-redis-cluster.aic.svc:6379
- graphql-redis-cluster-2.graphql-redis-cluster.aic.svc:6379
- graphql-redis-cluster-3.graphql-redis-cluster.aic.svc:6379
- graphql-redis-cluster-4.graphql-redis-cluster.aic.svc:6379
- graphql-redis-cluster-5.graphql-redis-cluster.aic.svc:6379
redis_password: redis-cluster-password
redis_cluster_name: graphql-limit-docs-redis-cluster
Apply the configuration to your cluster:
kubectl apply -f graphql-limit-count-ic.yaml
❶ 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.