Configure Rate Limiting
Understand how to set up rate limiting in Apache APISIX using various plugins to control access to your APIs and protect them from excessive requests.
Applies to:Apache APISIX 3.18.0
Roles:Platform engineerTime:10 minutesImpact:No restart, no traffic change
Goal
One route that serves two API plans at two different quotas: a consumer in the basic plan gets 2 requests per 30 seconds and a consumer in the premium plan gets 20, and every request over the quota is rejected with 429.
The quota lives on a consumer group, so changing a plan's limit is one write instead of one write per consumer. Redis-backed counters, sliding windows and per-rule limits are in the limit-count plugin reference (opens in Plugin Hub docs).
Before you begin
- You have an APISIX installation and cURL. Get APISIX installs one in Docker.
Set the variables this guide uses:
export APISIX_ADMIN_URL="http://127.0.0.1:9180"
export GATEWAY_URL="http://127.0.0.1:9080"
read -rsp "Admin API key: " APISIX_ADMIN_KEY && export APISIX_ADMIN_KEY && echo-
An Apache APISIX instance is running and its Admin API accepts your key. Check it with:
curl -s -o /dev/null -w '%{http_code}\n' "${APISIX_ADMIN_URL}/apisix/admin/routes" \ -H "X-API-KEY: ${APISIX_ADMIN_KEY}"Expected:
200. -
Its data plane is answering on the proxy port. Check it with:
curl -sI "${GATEWAY_URL}" | grep -i '^server'Expected:
Server: APISIX/3.18.0.
Steps
Three steps: two consumer groups that hold the plans, three consumers that belong to them, and one route that authenticates the caller so the gateway knows which plan to count against.
Step 1: Create a consumer group per plan
Put the quota on the plan, not on the consumer.
# 1. The basic plan: 2 requests per 30 seconds
curl -s "${APISIX_ADMIN_URL}/apisix/admin/consumer_groups/basic_plan" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"plugins": {
"limit-count": {
"count": 2,
"time_window": 30,
"rejected_code": 429
}
}
}'
# 2. The premium plan: 20 requests per 30 seconds
curl -s "${APISIX_ADMIN_URL}/apisix/admin/consumer_groups/premium_plan" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"plugins": {
"limit-count": {
"count": 20,
"time_window": 30,
"rejected_code": 429
}
}
}'{ "value": { "id": "basic_plan", "plugins": { "limit-count": { "count": 2, "time_window": 30, … } } } }
{ "value": { "id": "premium_plan", "plugins": { "limit-count": { "count": 20, "time_window": 30, … } } } }What changed: two quotas exist and no consumer is subject to either of them yet.
Step 2: Create a consumer in each plan
group_id is what puts a consumer on a plan; the key-auth credential is what lets the gateway recognise the consumer at request time.
# 1. Two consumers in the basic plan
curl -s "${APISIX_ADMIN_URL}/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"username": "JohnDoe",
"group_id": "basic_plan",
"plugins": { "key-auth": { "key": "john-key" } }
}'
curl -s "${APISIX_ADMIN_URL}/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"username": "JaneDoe",
"group_id": "basic_plan",
"plugins": { "key-auth": { "key": "jane-key" } }
}'
# 2. One consumer in the premium plan
curl -s "${APISIX_ADMIN_URL}/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"username": "FetchBot",
"group_id": "premium_plan",
"plugins": { "key-auth": { "key": "bot-key" } }
}'{ "value": { "username": "JohnDoe", "group_id": "basic_plan", … } }
{ "value": { "username": "JaneDoe", "group_id": "basic_plan", … } }
{ "value": { "username": "FetchBot", "group_id": "premium_plan", … } }What changed: three callers are now identifiable, two of them on the low quota and one on the high quota.
Step 3: Create the route and enable key-auth
The quota is per consumer group, so the gateway has to resolve the consumer before it can count. key-auth is what does that.
curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/rate-limiting" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"uri": "/internal",
"plugins": { "key-auth": {} },
"upstream": {
"nodes": { "mock.api7.ai:443": 1 },
"pass_host": "node",
"scheme": "https"
}
}'{ "value": { "id": "rate-limiting", "uri": "/internal", "plugins": { "key-auth": {} }, … } }What changed: /internal is live, closed to unkeyed callers, and metered per plan.
Validate
Send more requests than the basic plan allows and count what comes back. Ten requests as JohnDoe should split 2 accepted, 8 rejected:
resp=$(seq 10 | xargs -I{} curl -s -o /dev/null -w '%{http_code}\n' "${GATEWAY_URL}/internal" -H 'apikey: john-key') && \
echo "200: $(echo "$resp" | grep -c 200), 429: $(echo "$resp" | grep -c 429)"200: 2, 429: 8Thirty requests as FetchBot should split 20 and 10, on the same route, because the premium plan carries a different quota:
resp=$(seq 30 | xargs -I{} curl -s -o /dev/null -w '%{http_code}\n' "${GATEWAY_URL}/internal" -H 'apikey: bot-key') && \
echo "200: $(echo "$resp" | grep -c 200), 429: $(echo "$resp" | grep -c 429)"200: 20, 429: 10Roll back / Clean up
Remove everything this guide created, in reverse dependency order: the route first, then the consumers that reference the groups, then the groups.
curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/routes/rate-limiting" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/consumers/JohnDoe" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/consumers/JaneDoe" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/consumers/FetchBot" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/consumer_groups/basic_plan" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/consumer_groups/premium_plan" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"The first command takes /internal offline, so every request returns 404 from then on; each DELETE answers 200 with the etcd key it removed, which ends in the id ({"deleted":"1","key":"/apisix/routes/rate-limiting"}). To lift a quota without taking the path down, raise count on the group instead and leave the route alone.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
Every request returns 401, including the first one. | key-auth is enabled on the route and the key you send is not on any consumer, so the gateway cannot resolve a consumer — and without a consumer there is no group and no quota. | Send the key in the apikey header and compare it with plugins.key-auth.key on the consumer from step 2. |
| The second consumer in a plan is rejected on its first request. | limit-count counts against key (opens in Plugin Hub docs), which defaults to remote_addr: two consumers calling from the same address share one counter. | Set key to consumer_name to give each consumer its own quota, or to $remote_addr $consumer_name with key_type var_combination to keep both. |
More requests are accepted than count allows, or the rejected ones return 503 instead of 429. | Accepted: policy defaults to local, so each gateway instance counts on its own and the effective limit is roughly count × instances. 503: rejected_code was left out; its default is 503. | Accepted: set policy to redis, redis-cluster or redis-sentinel so the instances share one counter. 503: set rejected_code to 429, as step 1 does. |
Related
- Consumer Groups — why a quota belongs to a plan rather than to a consumer.
- Key Authentication — how the gateway resolves the caller this guide meters.
limit-countplugin reference (opens in Plugin Hub docs) — every field, including Redis-backed counters, sliding windows and multiple rules.limit-reqplugin reference (opens in Plugin Hub docs) — smooth a burst with a leaky bucket instead of a fixed window;limit-conn(opens in Plugin Hub docs) limits concurrent connections rather than requests, andgraphql-limit-count(opens in Plugin Hub docs) counts GraphQL operation depth.