Configure Rate Limiting
Configure fixed, shared Redis, and sliding-window quotas with deterministic checks.
Applies to:API7 Gateway 3.10.x
Roles:Platform engineer
Goal
Create three independent rate-limit routes: a fixed-window quota local to one gateway instance, a Redis-backed quota shared by gateway instances, and two sliding-window rules applied together. Validate exact rejection sequences with unique request keys.
These plugins enforce quotas within time windows. They do not provide a request queue or a burst parameter. Sliding windows smooth the classic fixed-window boundary burst; they do not guarantee an even request rate.
Before you begin
This procedure requires API7 Gateway 3.10.1 or later. That release added sync_interval to limit-count; the separate limit-count-advanced example remains supported for compatibility.
Set the control-plane and data-plane variables:
export API7_ADMIN_URL="https://localhost:7443"
export API7_GATEWAY_GROUP_ID="default" # only the group the quickstart creates; otherwise copy the gateway group's UUID from Gateway Groups in the Dashboard
read -rp "Gateway group name for ADC (if used): " API7_GATEWAY_GROUP_NAME && export API7_GATEWAY_GROUP_NAME
export GATEWAY_URL="http://127.0.0.1:9080"
read -rp "First direct gateway-instance URL (if testing Redis sharing): " GATEWAY_URL_A && export GATEWAY_URL_A
read -rp "Second direct gateway-instance URL (if testing Redis sharing): " GATEWAY_URL_B && export GATEWAY_URL_B
read -rp "Redis host reachable from every gateway instance: " REDIS_HOST && export REDIS_HOST
read -rp "Redis port: " REDIS_PORT && export REDIS_PORT
read -rp 'Redis password reference ($env:// or $secret://): ' REDIS_PASSWORD_REF && export REDIS_PASSWORD_REF
read -rsp "Dashboard token: " API7_ADMIN_KEY && export API7_ADMIN_KEY && echo-
Confirm that the control plane and selected gateway group are ready:
curl -ksS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}\n' "${API7_ADMIN_URL}/api/gateway_groups" -H "X-API-KEY: ${API7_ADMIN_KEY}" curl -ksS --connect-timeout 3 --max-time 10 "${API7_ADMIN_URL}/api/gateway_groups/${API7_GATEWAY_GROUP_ID}/instances" -H "X-API-KEY: ${API7_ADMIN_KEY}" | grep -o '"status": *"Healthy"' | wc -lExpected:
200, then1or more. -
The gateway can resolve and reach
httpbin.org:443. This is a public demonstration service, so its availability and behavior are outside API7 Gateway's control. -
Every gateway instance can reach the same Redis database at
${REDIS_HOST}:${REDIS_PORT}with the credential resolved fromREDIS_PASSWORD_REF. Configure the referenced environment variable or secret provider on every data plane before applying the route. Use an isolated test database or key namespace; this guide creates counters with short TTLs but does not administer Redis. -
curl,jq,grep,tr, and GNUenvsubstare available. -
For ADC, confirm label-selector support and reserve working files:
adc --version adc sync --help | grep -- '--label-selector' umask 077 for file in rate-limiting-adc.yaml.tmpl rate-limiting-adc.yaml rate-limiting-cleanup.yaml; do test ! -e "${file}" || { echo "Stop: ${file} already exists"; exit 1; }; doneADC resolves
--gateway-groupby display name; the compatibility Admin API uses the group's UUID. Enter the display name of the group identified byAPI7_GATEWAY_GROUP_ID.
Check the fixed Admin API identifiers before using PUT:
curl -ksS --connect-timeout 3 --max-time 10 -w '\nservice: %{http_code}\n' "${API7_ADMIN_URL}/apisix/admin/services/rate-limit-guide-service-id?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
for id in rate-limit-fixed-id rate-limit-redis-id rate-limit-sliding-id; do
curl -ksS --connect-timeout 3 --max-time 10 -w "\nroute ${id}: %{http_code}\n" "${API7_ADMIN_URL}/apisix/admin/routes/${id}?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
doneExpected: every resource is absent. A 200 response containing a resource means the ID is in use; stop or change every occurrence because PUT replaces that ID. Admin URL IDs deliberately differ from names. ADC names are not backend IDs; continue only if its diff creates exactly this guide's labeled resources.
Steps
Complete steps 1 and 2 for the Admin API, or skip them and complete step 3 for ADC. Do not apply both paths.
Step 1: Create the upstream service
curl -ksS --connect-timeout 3 --max-time 15 "${API7_ADMIN_URL}/apisix/admin/services/rate-limit-guide-service-id?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
-d '{"name":"rate-limit-guide-service","labels":{"guide":"rate-limiting"},"upstream":{"name":"default","type":"roundrobin","scheme":"https","pass_host":"node","nodes":[{"host":"httpbin.org","port":443,"weight":100}]}}'Step 2: Create fixed, Redis, and sliding routes
curl -ksS --connect-timeout 3 --max-time 15 "${API7_ADMIN_URL}/apisix/admin/routes/rate-limit-fixed-id?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
-d '{"name":"rate-limit-fixed","labels":{"guide":"rate-limiting"},"paths":["/rate/fixed"],"methods":["GET"],"service_id":"rate-limit-guide-service-id","plugins":{"proxy-rewrite":{"uri":"/get"},"limit-count":{"count":3,"time_window":60,"window_type":"fixed","key_type":"var","key":"http_x_rate_key","policy":"local","rejected_code":429,"rejected_msg":"Too Many Requests","show_limit_quota_header":true}}}'
jq -n --arg host "${REDIS_HOST}" --argjson port "${REDIS_PORT}" --arg password "${REDIS_PASSWORD_REF}" '{name:"rate-limit-redis",labels:{guide:"rate-limiting"},paths:["/rate/redis"],methods:["GET"],service_id:"rate-limit-guide-service-id",plugins:{"proxy-rewrite":{uri:"/get"},"limit-count":{count:1,time_window:60,window_type:"fixed",key_type:"var",key:"http_x_rate_key",policy:"redis",redis_host:$host,redis_port:$port,redis_password:$password,redis_database:0,sync_interval:-1,allow_degradation:false,rejected_code:429,show_limit_quota_header:true}}}' \
| curl -ksS --connect-timeout 3 --max-time 15 "${API7_ADMIN_URL}/apisix/admin/routes/rate-limit-redis-id?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" --data-binary @-
curl -ksS --connect-timeout 3 --max-time 15 "${API7_ADMIN_URL}/apisix/admin/routes/rate-limit-sliding-id?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
-d '{"name":"rate-limit-sliding","labels":{"guide":"rate-limiting"},"paths":["/rate/sliding"],"methods":["GET"],"service_id":"rate-limit-guide-service-id","plugins":{"proxy-rewrite":{"uri":"/get"},"limit-count-advanced":{"window_type":"sliding","policy":"local","rejected_code":429,"show_limit_quota_header":true,"rules":[{"count":3,"time_window":60,"key":"$http_x_rate_key-minute","header_prefix":"Minute"},{"count":10,"time_window":3600,"key":"$http_x_rate_key-hour","header_prefix":"Hour"}]}}}'The fixed and sliding local counters are shared by workers in one gateway instance, but not by different gateway instances. The Redis route uses direct synchronization on each request; allow_degradation: false returns an error rather than allowing unmetered traffic if the counter backend fails. Both sliding rules apply sequentially to the same request.
Step 3: Apply one complete desired state with ADC
Skip this alternative if you completed steps 1 and 2. Save all three routes in one restricted template:
services:
- name: rate-limit-guide-service
labels: {guide: rate-limiting}
upstream:
name: default
scheme: https
pass_host: node
nodes: [{host: httpbin.org, port: 443, weight: 100}]
routes:
- name: rate-limit-fixed
labels: {guide: rate-limiting}
uris: [/rate/fixed]
methods: [GET]
plugins:
proxy-rewrite: {uri: /get}
limit-count: {count: 3, time_window: 60, window_type: fixed, key_type: var, key: http_x_rate_key, policy: local, rejected_code: 429, rejected_msg: Too Many Requests, show_limit_quota_header: true}
- name: rate-limit-redis
labels: {guide: rate-limiting}
uris: [/rate/redis]
methods: [GET]
plugins:
proxy-rewrite: {uri: /get}
limit-count:
count: 1
time_window: 60
window_type: fixed
key_type: var
key: http_x_rate_key
policy: redis
redis_host: "${REDIS_HOST}"
redis_port: ${REDIS_PORT}
redis_password: "${REDIS_PASSWORD_REF}"
redis_database: 0
sync_interval: -1
allow_degradation: false
rejected_code: 429
show_limit_quota_header: true
- name: rate-limit-sliding
labels: {guide: rate-limiting}
uris: [/rate/sliding]
methods: [GET]
plugins:
proxy-rewrite: {uri: /get}
limit-count-advanced:
window_type: sliding
policy: local
rejected_code: 429
show_limit_quota_header: true
rules:
- {count: 3, time_window: 60, key: '$http_x_rate_key-minute', header_prefix: Minute}
- {count: 10, time_window: 3600, key: '$http_x_rate_key-hour', header_prefix: Hour}Render only the three Redis values, then lint, validate, inspect, and apply the selected state:
envsubst '${REDIS_HOST} ${REDIS_PORT} ${REDIS_PASSWORD_REF}' < rate-limiting-adc.yaml.tmpl > rate-limiting-adc.yaml
adc lint -f rate-limiting-adc.yaml
adc validate -f rate-limiting-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=rate-limiting
adc diff -f rate-limiting-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=rate-limiting
adc sync -f rate-limiting-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=rate-limitingExpected: validation succeeds and the reviewed diff affects only the labeled service and three routes. The rendered file contains a password reference rather than its secret value; keep its restricted permissions and remove it during cleanup.
Step 4: Add an anonymous consumer
To give unauthenticated callers a lower quota than authenticated callers, configure limit-count on an anonymous consumer, then set an authentication plugin's anonymous_consumer field to that consumer's username. The complete Limit Count anonymous-consumer example (opens in Plugin Hub docs) creates both consumer policies, connects the anonymous consumer through key-auth, and validates their quotas separately. The same pattern is supported by basic-auth, jwt-auth, and hmac-auth.
Validate
Address one gateway instance or a sticky endpoint for local-policy checks. Use a new key and require three successes followed by one rejection:
RATE_KEY="fixed-$(date +%s)"
FIXED_CODES=()
for request in {1..4}; do
FIXED_CODES+=("$(curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}' "${GATEWAY_URL}/rate/fixed" -H "X-Rate-Key: ${RATE_KEY}")")
done
test "${FIXED_CODES[*]}" = "200 200 200 429"
FIXED_HEADERS=$(curl -sS --connect-timeout 3 --max-time 15 -D - -o /dev/null "${GATEWAY_URL}/rate/fixed" -H "X-Rate-Key: ${RATE_KEY}" | tr -d '\r')
printf '%s\n' "${FIXED_HEADERS}" | grep -qi '^X-RateLimit-Limit: 3$'
printf '%s\n' "${FIXED_HEADERS}" | grep -qi '^X-RateLimit-Remaining: 0$'
printf '%s\n' "${FIXED_HEADERS}" | grep -qiE '^X-RateLimit-Reset: [0-9]+(\.[0-9]+)?$'To prove the Redis quota is shared, the two URLs must reach different instances directly. Consume the one-request quota on A and require B to reject the same unique key:
test -n "${GATEWAY_URL_A}" && test -n "${GATEWAY_URL_B}" && test "${GATEWAY_URL_A}" != "${GATEWAY_URL_B}"
RATE_KEY="redis-$(date +%s)"
test "$(curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}' "${GATEWAY_URL_A}/rate/redis" -H "X-Rate-Key: ${RATE_KEY}")" = 200
test "$(curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}' "${GATEWAY_URL_B}/rate/redis" -H "X-Rate-Key: ${RATE_KEY}")" = 429If only one instance is available, the same two requests validate Redis connectivity and quota behavior on that instance, but do not prove cross-instance sharing.
Finally, require the short sliding rule to reject the fourth request and expose distinct headers for both rules:
RATE_KEY="sliding-$(date +%s)"
SLIDING_CODES=()
for request in {1..4}; do
SLIDING_CODES+=("$(curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}' "${GATEWAY_URL}/rate/sliding" -H "X-Rate-Key: ${RATE_KEY}")")
done
test "${SLIDING_CODES[*]}" = "200 200 200 429"
SLIDING_HEADERS=$(curl -sS --connect-timeout 3 --max-time 15 -D - -o /dev/null "${GATEWAY_URL}/rate/sliding" -H "X-Rate-Key: ${RATE_KEY}" | tr -d '\r')
printf '%s\n' "${SLIDING_HEADERS}" | grep -qi '^X-Minute-RateLimit-Limit: 3$'
printf '%s\n' "${SLIDING_HEADERS}" | grep -qi '^X-Hour-RateLimit-Limit: 10$'Lowering Redis round trips with a positive sync_interval trades exactness for bounded staleness across workers and instances. Keep it at least 0.1 and below time_window; see Improve Rate Limiting Accuracy before enabling it.
Roll back / Clean up
For the Admin API path, delete routes before their service:
for id in rate-limit-sliding-id rate-limit-redis-id rate-limit-fixed-id; do
curl -ksS --connect-timeout 3 --max-time 15 -X DELETE "${API7_ADMIN_URL}/apisix/admin/routes/${id}?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
done
curl -ksS --connect-timeout 3 --max-time 15 -X DELETE "${API7_ADMIN_URL}/apisix/admin/services/rate-limit-guide-service-id?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"For ADC, review and apply an empty selected state:
services: []adc lint -f rate-limiting-cleanup.yaml
adc diff -f rate-limiting-cleanup.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=rate-limiting
adc sync -f rate-limiting-cleanup.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=rate-limitingRemove the working files and shell variables:
rm -f rate-limiting-adc.yaml.tmpl rate-limiting-adc.yaml rate-limiting-cleanup.yaml
unset REDIS_PASSWORD_REF RATE_KEYTroubleshoot
- A local quota allows more requests than expected: make sure the validation reaches one instance. Each gateway instance owns an independent
localquota. - The Redis route returns
500: verify network access, port, password, database, and TLS settings from every gateway instance. This guide fails closed becauseallow_degradationis false. - The two-instance check is inconclusive: prove that A and B are different data-plane instances and use the same unique key inside one window. A load balancer URL by itself cannot prove which instance served either request.
- A request bypasses its rule: with
key_type: var, usehttp_x_rate_keywithout$; insiderules, use$http_x_rate_key. Always sendX-Rate-Key. - Advanced headers are missing: keep
show_limit_quota_header: trueand uniqueheader_prefixvalues. Multiple rules otherwise use numeric prefixes. - The sliding route never applies (every request 404s): each rule's
keymust be a distinct string across the wholerulesarray — two rules with the exact samekeyfail configuration checks and the route is never synced to the data plane. Give each window its own key, for example by appending a fixed per-rule suffix ($http_x_rate_key-minute,$http_x_rate_key-hour) rather than reusing the same key expression verbatim. - ADC proposes unrelated changes: stop and verify the gateway-group display name,
guide=rate-limitingselector, labels, and complete desired-state file.