Kong and API Gateway Rate Limit Plugins
A gateway plugin is the first enforcement tier that knows who the caller is, which makes it the cheapest place to express “free tier gets 10 per second, pro gets 100” without touching application code. The catch is a storage policy that quietly decides whether your limit is exact or merely approximate, and a precedence model that decides which of four overlapping plugin configurations actually applies. Both belong to the tier discussion in edge and gateway enforcement; this page covers the configuration mechanics and the failure cases they produce.
The problem in concrete numbers
You attach a rate limiting plugin globally at 1,000 per minute, another to the /v1/reports route at 60 per minute, and a third to your enterprise consumer at 10,000 per minute. A request from that consumer to /v1/reports is subject to exactly one of those numbers — the most specific one that matches — not to all three. Teams routinely expect the intersection and get the override, then discover during an incident that the global ceiling they believed in was never applied to their largest customer.
Storage adds the second surprise. With the local policy each gateway node counts independently, so three nodes serving one consumer enforce 3× the configured limit — the same multiplication that affects proxy-level limiters. Switching to a shared Redis makes the number exact at the price of a round trip per request.
Decision matrix: storage policy
| Policy | Accuracy | Latency added | Survives restart | Right for |
|---|---|---|---|---|
local |
Per node; total = limit × nodes | ~0 | No | Coarse shielding on a single node, dev |
cluster (database counters) |
Exact, but writes to the config database | 1 DB round trip; heavy at scale | Yes | Small deployments already sized for DB writes |
redis |
Exact fleet-wide | 0.5–2 ms | Yes, with persistence | Production, any published limit |
redis + sync_rate batching |
Near-exact, small overshoot | Amortised | Yes | Very high request rates where per-request writes are too costly |
The cluster policy is the one to avoid at scale: it turns every request into a write against the database that also holds your gateway configuration, which is the last component you want under load. Use Redis, dedicated to limiting, with an eviction policy that cannot delete live counters.
Step-by-step configuration
# kong.yaml (declarative config) — per-consumer limits with shared Redis counters.
_format_version: "3.0"
consumers:
- username: acct_free_demo
keyauth_credentials: [{ key: "demo_key_123" }]
- username: acct_enterprise_7
keyauth_credentials: [{ key: "ent_key_789" }]
services:
- name: api
url: http://origin.internal:8080
routes:
- name: v1
paths: ["/v1"]
plugins:
# 1. Identify the caller first — without this every request is anonymous.
- name: key-auth
service: api
config: { key_names: ["X-API-Key"], hide_credentials: true }
# 2. Default plan: applies to any consumer without an override.
- name: rate-limiting
service: api
config:
second: 10
minute: 300 # two windows: burst control plus sustained budget
policy: redis
redis:
host: redis-ratelimit.internal
port: 6379
database: 3 # dedicated DB, never shared with a cache
timeout: 200 # ms — fail fast rather than adding latency
limit_by: consumer # not ip: an authenticated identity is the unit
fault_tolerant: true # allow traffic if Redis is unreachable (read paths)
hide_client_headers: false
error_code: 429
error_message: "API rate limit exceeded"
# 3. Enterprise override: attached to the consumer, so it wins over the service default.
- name: rate-limiting
consumer: acct_enterprise_7
config:
second: 200
minute: 10000
policy: redis
redis: { host: redis-ratelimit.internal, port: 6379, database: 3 }
limit_by: consumer
fault_tolerant: false # this customer's writes are expensive: fail closedThe two-window configuration (second plus minute) is the pattern to copy. A single per-minute limit lets a client spend the entire budget in the first second and stall for 59; a single per-second limit gives no useful monthly shape. Together they bound both burst and sustained use, and the gateway reports the tighter of the two in its headers.
Gotchas and edge cases
limit_by: ipbehind a load balancer counts the balancer. Set the gateway’s trusted-proxy configuration so the real client address is recovered, or key by consumer instead.- Anonymous traffic has no consumer. Requests that fail authentication never reach the consumer-scoped plugin, so an unauthenticated flood is limited only by whatever global or IP-based policy exists. Add one explicitly.
- Plugin ordering matters. Authentication must run before the rate limiter, or
limit_by: consumersilently degrades to a single shared bucket for everyone. fault_tolerant: trueis a silent unlimited mode. When Redis is unreachable the gateway admits everything. Alert on the plugin’s Redis error counter, not just on 429 rate.- Header stripping downstream. Some deployments strip unknown headers at an outer proxy, removing exactly the quota headers clients need. Assert their presence at the public edge, not at the gateway.
- Changing a consumer’s plan does not reset their counter. Upgrading mid-window leaves the old window’s count in place; the new limit applies from the next window. Communicate that, or reset the key explicitly on upgrade.
- One Redis for cache and counters. An eviction policy tuned for caching (
allkeys-lru) will happily delete live rate limit keys under memory pressure. Use a dedicated database andvolatile-ttl.
Verification and testing
# 1. Precedence: does the enterprise override actually apply?
curl -is -H "X-API-Key: ent_key_789" https://api.example.com/v1/ping | \
grep -i "ratelimit-limit"
# expect the override's numbers (200/s, 10000/min), not the service default
# 2. Fleet exactness: hammer one key and count accepted responses.
hey -z 20s -c 30 -H "X-API-Key: demo_key_123" https://api.example.com/v1/ping | \
grep -E "\[200\]|\[429\]"
# expect ~200 accepted over 20 s (10/s) regardless of how many gateway nodes run
# 3. Failure policy: block Redis from one node and observe the configured behaviour.
sudo iptables -A OUTPUT -p tcp --dport 6379 -j DROP
curl -s -o /dev/null -w "%{http_code}\n" -H "X-API-Key: demo_key_123" https://api.example.com/v1/ping
# fault_tolerant true -> 200 (and the Redis error counter must climb)
# fault_tolerant false -> 500 or 429 depending on version; either way, deliberate
sudo iptables -D OUTPUT -p tcp --dport 6379 -j DROPTest one is worth running in CI against staging every time plugin configuration changes, because precedence mistakes are invisible until a large customer is affected. Wire it into the CI verification job alongside your header assertions.
Frequently Asked Questions
Should limits live in the gateway or in the application?
Put per-plan request limits in the gateway when plans are already modelled there — it keeps enforcement out of application code and applies uniformly across services. Keep anything that depends on request cost, account balance, or billing state in the application, because a gateway cannot see it.
Why do my per-consumer limits apply to everyone at once?
Almost always plugin ordering: if the rate limiter runs before authentication, there is no consumer yet, so every request falls into the same bucket. Ensure the authentication plugin has a lower execution order, and check that credentials are actually being matched rather than silently ignored.
Is the database-backed cluster policy safe in production?
Only at low request rates. It writes a counter update per request into the same database that stores gateway configuration, which becomes a bottleneck and a shared failure domain exactly when traffic is heaviest. Redis with a dedicated database is the production choice.
How do I give one customer a higher limit without changing the default?
Attach a second plugin instance to that consumer. Because consumer-scoped configuration is more specific than service or global configuration, it wins for that caller only, and the default continues to apply to everyone else. Avoid raising the global number as a shortcut — it silently widens the limit for every client.
Related
- Edge & Gateway Enforcement — the parent guide on where enforcement belongs.
- Tiered Access & Quota Enforcement — the plan model gateway consumers stand in for.
- Envoy Global Rate Limit Service Setup — the mesh equivalent of a gateway plugin.
- API Key Scoping and Rate Limits — designing the key that the gateway counts by.