Improve Rate Limiting Accuracy Across Gateway Instances
Configure limit-count-advanced sync_interval and window_type, then run deterministic checks for the staleness window delayed Redis synchronization introduces and how fixed and sliding windows differ.
Applies to:API7 Gateway 3.10.x
Roles:Platform engineer
Goal
Create two limit-count-advanced (opens in Plugin Hub docs) routes that share a quota across
gateway instances through Redis — one window_type: fixed, one window_type: sliding — and run
deterministic checks against your own instances that show:
- A brief staleness window where delayed synchronization lets more requests through than the
configured
count, and that it clears at the next synchronization. - The documented behavioral difference between the two window types: a fixed window resets fully
at each boundary, while a sliding window counts recent requests within the last
time_windowregardless of when that window began.
These checks confirm the mechanics documented in the plugin's own configuration reference. They do
not measure an error percentage for your deployment — the size of the staleness window depends on
your instance count, traffic rate, and sync_interval, which only you can measure in your own
environment. limit-count-advanced is an API7 Enterprise plugin, available since v3.2.16.4;
sync_interval is documented as Enterprise-only.
This guide is written against limit-count-advanced. From 3.10.1 every feature it demonstrates —
sliding windows, rules, sync_interval, and the redis / redis-sentinel policies — is available
directly in limit-count (opens in Plugin Hub docs), which is the plugin to reach for in new configuration.
limit-count-advanced is retained as a compatibility wrapper, so the procedure below still works
unchanged. See Configure Rate Limiting.
Before you begin
Set the variables used in this guide:
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: " GATEWAY_URL_A && export GATEWAY_URL_A
read -rp "Second direct gateway-instance URL: " 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, then2or more. The staleness check in this guide requires two distinct data plane instances; the window-reset checks work with one. -
GATEWAY_URL_AandGATEWAY_URL_Breach two different gateway instances directly (not through a load balancer that could route both requests to the same instance). -
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 routes. Use an isolated test database or key namespace; this guide creates counters with a shorttime_windowbut does not administer Redis. -
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. -
curl,jq,grep,tr, and GNUenvsubstare available. -
For ADC, confirm label-selector support and reserve the working files:
adc --version adc sync --help | grep -- '--label-selector' for file in rate-accuracy-adc.yaml.tmpl rate-accuracy-adc.yaml rate-accuracy-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-accuracy-guide-service-id?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
for id in rate-accuracy-fixed-id rate-accuracy-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. For ADC, 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-accuracy-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-accuracy-guide-service","labels":{"guide":"rate-limiting-accuracy"},"upstream":{"name":"default","type":"roundrobin","scheme":"https","pass_host":"node","nodes":[{"host":"httpbin.org","port":443,"weight":100}]}}'Step 2: Create fixed and sliding routes sharing a Redis quota
Both routes use count: 1, time_window: 5, and sync_interval: 1 — a sync interval well inside
the documented [0.1, time_window) range — so a one-second gap between two requests is enough to
observe delayed synchronization, and a two-second wait is enough to observe the next one.
sync_interval (opens in Plugin Hub docs) defaults to -1,
which synchronizes counter data to Redis on every request instead of on a timer; this guide sets an
explicit positive value specifically to make the staleness window in the next section observable,
not because -1 is unsafe for production use.
jq -n --arg host "${REDIS_HOST}" --argjson port "${REDIS_PORT}" --arg password "${REDIS_PASSWORD_REF}" \
'{name:"rate-accuracy-fixed",labels:{guide:"rate-limiting-accuracy"},paths:["/rate-accuracy/fixed"],methods:["GET"],service_id:"rate-accuracy-guide-service-id",plugins:{"proxy-rewrite":{uri:"/get"},"limit-count-advanced":{window_type:"fixed",count:1,time_window:5,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-accuracy-fixed-id?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" --data-binary @-
jq -n --arg host "${REDIS_HOST}" --argjson port "${REDIS_PORT}" --arg password "${REDIS_PASSWORD_REF}" \
'{name:"rate-accuracy-sliding",labels:{guide:"rate-limiting-accuracy"},paths:["/rate-accuracy/sliding"],methods:["GET"],service_id:"rate-accuracy-guide-service-id",plugins:{"proxy-rewrite":{uri:"/get"},"limit-count-advanced":{window_type:"sliding",count:1,time_window:5,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-accuracy-sliding-id?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" --data-binary @-allow_degradation: false returns an error rather than allowing unmetered traffic if Redis becomes
unreachable, matching Configure Rate Limiting.
Step 3: Apply one complete desired state with ADC
Skip this alternative if you completed steps 1 and 2. Save both routes in one restricted template:
services:
- name: rate-accuracy-guide-service
labels: {guide: rate-limiting-accuracy}
upstream:
name: default
scheme: https
pass_host: node
nodes: [{host: httpbin.org, port: 443, weight: 100}]
routes:
- name: rate-accuracy-fixed
labels: {guide: rate-limiting-accuracy}
uris: [/rate-accuracy/fixed]
methods: [GET]
plugins:
proxy-rewrite: {uri: /get}
limit-count-advanced:
window_type: fixed
count: 1
time_window: 5
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-accuracy-sliding
labels: {guide: rate-limiting-accuracy}
uris: [/rate-accuracy/sliding]
methods: [GET]
plugins:
proxy-rewrite: {uri: /get}
limit-count-advanced:
window_type: sliding
count: 1
time_window: 5
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: trueRender only the three Redis values, then lint, validate, inspect, and apply the selected state:
envsubst '${REDIS_HOST} ${REDIS_PORT} ${REDIS_PASSWORD_REF}' < rate-accuracy-adc.yaml.tmpl > rate-accuracy-adc.yaml
adc lint -f rate-accuracy-adc.yaml
adc validate -f rate-accuracy-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-accuracy
adc diff -f rate-accuracy-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-accuracy
adc sync -f rate-accuracy-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-accuracyExpected: validation succeeds and the reviewed diff affects only the labeled service and two routes. The rendered file contains a password reference rather than its secret value; keep its restricted permissions and remove it during cleanup.
Validate
Confirm each route enforces its quota on one instance
Use a new key per route and require one success followed by one rejection:
FIXED_KEY="fixed-$(date +%s)"
test "$(curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}' "${GATEWAY_URL}/rate-accuracy/fixed" -H "X-Rate-Key: ${FIXED_KEY}")" = 200
test "$(curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}' "${GATEWAY_URL}/rate-accuracy/fixed" -H "X-Rate-Key: ${FIXED_KEY}")" = 429
SLIDING_KEY="sliding-$(date +%s)"
test "$(curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}' "${GATEWAY_URL}/rate-accuracy/sliding" -H "X-Rate-Key: ${SLIDING_KEY}")" = 200
test "$(curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}' "${GATEWAY_URL}/rate-accuracy/sliding" -H "X-Rate-Key: ${SLIDING_KEY}")" = 429Observe the delayed-synchronization staleness window
With a positive sync_interval, an instance does not push every admission to Redis immediately —
it flushes on its own one-second schedule. A second instance that has not yet flushed or re-read
still sees the quota as available, so it can admit a request that the first instance already
consumed. Reproduce this against the fixed route, where a full reset at the window boundary
cannot be confused with the staleness effect:
STALE_KEY="stale-$(date +%s)"
test "$(curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}' "${GATEWAY_URL_A}/rate-accuracy/fixed" -H "X-Rate-Key: ${STALE_KEY}")" = 200
CODE_B_IMMEDIATE=$(curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}' "${GATEWAY_URL_B}/rate-accuracy/fixed" -H "X-Rate-Key: ${STALE_KEY}")
sleep 2
CODE_B_AFTER_SYNC=$(curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}' "${GATEWAY_URL_B}/rate-accuracy/fixed" -H "X-Rate-Key: ${STALE_KEY}")
echo "B immediately after A: ${CODE_B_IMMEDIATE}; B after a two-second wait: ${CODE_B_AFTER_SYNC}"
test "${CODE_B_AFTER_SYNC}" = 429Expected: CODE_B_AFTER_SYNC is 429 — after a synchronization has had time to complete, instance B
correctly sees the quota as exhausted. CODE_B_IMMEDIATE is commonly 200, demonstrating that B
admitted a second request against a count: 1 quota before its next synchronization. If
CODE_B_IMMEDIATE is already 429, the two curl commands took longer than sync_interval to run
on your system; rerun with a larger sync_interval (and a proportionally larger time_window) to
widen the window, or issue the two requests closer together. Either outcome for
CODE_B_IMMEDIATE is consistent with the documented behavior — this check demonstrates that the
staleness window exists and clears at the next synchronization; it does not bound its size for your
deployment.
Fixed and sliding windows reset differently
This guide does not assert a specific timing for the boundary itself, because window alignment
depends on when the counter was first created. The limit-count-advanced overview states the
qualitative difference directly: a fixed window "tracks requests in non-overlapping time
intervals," rejecting until "the next time window begins," while a sliding window "tracks requests
in overlapping intervals... counting recent requests within the last configured time period,
regardless of when the interval began." In practice this means a fixed window can admit a fresh
count immediately after every reset — including right after a previous burst — while a sliding
window keeps counting that previous burst against the new admission until it fully ages out. The
plugin's own documentation notes that X-RateLimit-Remaining can occasionally show a small negative
value under the sliding algorithm, which it describes as an expected property of the approximation,
not a defect.
Roll back / Clean up
For the Admin API path, delete routes before their service:
for id in rate-accuracy-sliding-id rate-accuracy-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-accuracy-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-accuracy-cleanup.yaml
adc diff -f rate-accuracy-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-accuracy
adc sync -f rate-accuracy-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-accuracyRemove the working files and shell variables:
rm -f rate-accuracy-adc.yaml.tmpl rate-accuracy-adc.yaml rate-accuracy-cleanup.yaml
unset REDIS_PASSWORD_REF FIXED_KEY SLIDING_KEY STALE_KEYTroubleshoot
CODE_B_IMMEDIATEis always429, never200: the twocurlcommands are taking longer thansync_intervalto reach the gateway, orGATEWAY_URL_A/GATEWAY_URL_Breach the same instance. Confirm they are different instances and increasesync_intervaltemporarily to widen the window.CODE_B_AFTER_SYNCis still200after the two-second wait: confirm both instances resolve the sameredis_host,redis_port, andredis_database, and thatsync_intervalwas applied — re-check the route with aGETafter thePUT.- The Redis route returns
500: verify network access, port, password, database, and TLS settings from every gateway instance. This guide fails closed becauseallow_degradationisfalse. - A request bypasses its rule: with
key_type: var, usehttp_x_rate_keywithout$. Always sendX-Rate-Key. - ADC proposes unrelated changes: stop and verify the gateway-group display name,
guide=rate-limiting-accuracyselector, labels, and complete desired-state file.