Docs
API7 GatewayManage traffic and APIsTraffic ManagementConfigure Rate Limiting

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 -l

    Expected: 200, then 1 or 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 from REDIS_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 GNU envsubst are 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; }; done

    ADC resolves --gateway-group by display name; the compatibility Admin API uses the group's UUID. Enter the display name of the group identified by API7_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}"
done

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

rate-limiting-adc.yaml.tmpl
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-limiting

Expected: 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}")" = 429

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

rate-limiting-cleanup.yaml
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-limiting

Remove 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_KEY

Troubleshoot

  • A local quota allows more requests than expected: make sure the validation reaches one instance. Each gateway instance owns an independent local quota.
  • The Redis route returns 500: verify network access, port, password, database, and TLS settings from every gateway instance. This guide fails closed because allow_degradation is 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, use http_x_rate_key without $; inside rules, use $http_x_rate_key. Always send X-Rate-Key.
  • Advanced headers are missing: keep show_limit_quota_header: true and unique header_prefix values. Multiple rules otherwise use numeric prefixes.
  • The sliding route never applies (every request 404s): each rule's key must be a distinct string across the whole rules array — two rules with the exact same key fail 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-limiting selector, labels, and complete desired-state file.