NGINX vs Kong for Rate Limiting
Rate limiting looks like a solved problem until you run it across more than one node. On a single box, both NGINX and Kong will happily cap a client to 100 requests per second. Put four replicas behind a load balancer and the naive NGINX setup silently allows 400, while a business that promised a customer “1,000 requests per minute” finds the number enforced is anywhere between 1,000 and 4,000 depending on which replica the load balancer picked. This comparison puts NGINX (limit_req / limit_conn, leaky bucket) head to head with Kong 3.x (rate-limiting and rate-limiting-advanced, fixed and sliding window over Redis) on the axes that actually matter in production: accuracy, distributed counters, burst handling, and per-consumer scoping.
Prerequisite concepts
This page assumes you have read the rate limiting and throttling strategies overview and understand where a limiter sits in the middleware chain — after authentication so that quotas are scoped to an identified consumer, and before expensive transformation work so rejected requests cost as little as possible. The distributed-counter question at the heart of this comparison is treated in depth in dynamic rate limiting with Redis backends; read it if you plan to run Kong’s advanced plugin at scale, because the Redis topology decisions there determine whether your counters stay accurate under load.
The core divergence: where the counter lives
Everything downstream flows from one architectural fact. NGINX stores its limiter state in a named shared-memory zone local to a single NGINX instance. Kong’s advanced limiter stores state in an external Redis cluster shared by every data-plane node. That single difference dictates accuracy, failure behavior, and operational cost.
NGINX: leaky bucket with limit_req
NGINX’s limit_req implements a leaky bucket. You declare a memory zone and a drain rate; the burst parameter sets how many excess requests can queue before NGINX rejects with 503. nodelay forwards the burst immediately instead of pacing it, while still counting against the rate.
# NGINX Plus R32+ — leaky-bucket rate limit keyed per client IP
http {
# 10 MB zone holds ~160k unique keys; rate is the steady drain
limit_req_zone $binary_remote_addr zone=perip:10m rate=100r/s;
limit_req_status 429;
# Optional: cap concurrent connections per key
limit_conn_zone $binary_remote_addr zone=connzone:10m;
server {
listen 443 ssl;
location /api/ {
# burst=200 bucket depth; nodelay = forward burst without pacing
limit_req zone=perip burst=200 nodelay;
limit_conn connzone 50;
proxy_pass http://api_upstream;
}
}
}
The strengths are speed and simplicity: this runs in NGINX’s C event loop and adds microseconds. The rate=100r/s is enforced with sub-request granularity — NGINX internally tracks to the millisecond, so 100r/s really means one request per 10 ms, not a bucket that refills once a second. To key per consumer instead of per IP, extract a JWT claim or API key into a variable ($http_x_api_key or a map over a decoded claim) and use it as the zone key — but NGINX has no notion of consumer tiers, so per-plan quotas mean one zone per tier and a map to route keys into them. The hard limit is fleet accuracy: the perip zone is local to each NGINX process. Three hosts means three independent 100 r/s buckets and an effective 300 r/s ceiling. NGINX open source offers no cross-node coordination; you must front it with consistent-hash load balancing so a given key always lands on the same node — fragile, and it breaks the moment you rebalance.
Kong: fixed and sliding window over Redis
Kong ships two plugins. The community rate-limiting plugin does fixed-window counting; the rate-limiting-advanced plugin (Kong’s enterprise/advanced limiter) adds sliding windows and a proper Redis policy. Both are consumer-native: once an authentication plugin has identified the caller, the limiter scopes its counter to that consumer with no extra wiring.
# Kong 3.x — rate-limiting-advanced: sliding window, per-consumer, Redis-backed
_format_version: "3.0"
plugins:
- name: rate-limiting-advanced
config:
limit: [1000] # 1000 requests...
window_size: [60] # ...per 60s window
window_type: sliding # rolling window, not calendar-aligned
identifier: consumer # scope counter to the authenticated consumer
sync_rate: 0.2 # push local deltas to Redis every 200ms
strategy: redis
redis:
host: redis.internal
port: 6379
timeout: 200
database: 0
# If Redis is unreachable, fall back to local counting rather than 500
disable_penalty: false
retry_after_jitter_max: 5 # jitter Retry-After to avoid synchronized retries
window_type: sliding is the accuracy win over a fixed calendar window. A fixed window resets on the minute boundary, so a client can send 1,000 requests at 12:00:59 and another 1,000 at 12:01:00 — 2,000 requests in one second while never breaking the “1,000 per minute” rule. The sliding window weights the previous window’s count by how far into the current window you are, smoothing that boundary spike. The sync_rate knob is the crucial accuracy-versus-load trade-off: at 0.2 each node counts locally and flushes deltas to Redis every 200 ms, so counters can briefly drift by the traffic one node sees in that interval. Set sync_rate: 0 for synchronous per-request Redis updates (exact, but a Redis round trip on every request) or a higher value for less Redis load and looser accuracy. Kong also returns RateLimit-Remaining and Retry-After response headers automatically, so clients can self-throttle — something NGINX does not provide out of the box.
Decision matrix
| Criterion | NGINX (limit_req) | Kong 3.x (rate-limiting-advanced) |
|---|---|---|
| Algorithm | Leaky bucket (drain + burst) | Fixed or sliding window |
| Counter locality | Per-instance shared-memory zone | Shared Redis across all data planes |
| Fleet accuracy | Multiplies by node count unless hash-pinned | Accurate across replicas via Redis |
| Boundary spikes | Smoothed by leaky drain | Sliding window smooths window edges |
| Per-consumer scoping | Manual: key on a variable, no tiers | Native identifier: consumer, per-plan overrides |
| Client feedback | None by default (limit_req_status only) |
RateLimit-Remaining, Retry-After headers |
| Latency added | Microseconds (in C event loop) | ~0.3–3 ms (local sync) or a Redis RTT (sync_rate: 0) |
| External dependency | None | Redis (a new failure domain) |
| Best for | Coarse IP limits at a fast single edge | Per-consumer quotas, distributed fleets, SLAs |
Pick NGINX when you already run it, the limit is coarse and IP-based, and a single instance (or a hash-pinned pool) covers your traffic. Pick Kong when the number you enforce has to match a contract across many replicas, when quotas are per-consumer, or when clients need remaining-quota headers. The decision is rarely about throughput — it is about whether your accuracy requirement survives horizontal scaling.
Gotchas and failure signals
- NGINX limit multiplied by replicas. The classic silent failure: a customer promised 1,000 r/m gets 4,000 across a 4-node fleet. Symptom: reported limit never triggers under real load. Fix with consistent-hash balancing to pin keys, or move the limit to Kong+Redis.
- NGINX zone exhaustion. When the
10mzone fills with unique keys, NGINX evicts the least-recently-used entries, and evicted keys reset to zero — attackers can churn keys to dodge limits. Size the zone for your real cardinality. nodelaydefeats shaping.burst=200 nodelayforwards all 200 instantly; withoutnodelayNGINX paces them at the drain rate. If you added burst to protect a fragile upstream,nodelaysends exactly the spike you were guarding against.- Kong
sync_ratedrift. A highsync_rateunder a traffic spike lets each node overshoot before it flushes to Redis. If your SLA is hard, lowersync_ratetoward 0 and accept the Redis round-trip cost. - Kong fixed-window boundary burst. Using the basic
rate-limitingplugin withpolicy: redisstill uses a fixed window — the minute-boundary doubling applies. Switch torate-limiting-advancedwithwindow_type: slidingwhen boundary accuracy matters. - Redis as a new failure domain. If Redis latency spikes, a synchronous limiter adds that latency to every request. Set a tight
timeoutand a sane fallback (disable_penalty/ fault-tolerant mode) so a Redis blip degrades accuracy, not availability.
Validation
- Enforced limit measured against a multi-node deployment, not a single instance, using a load generator.
- NGINX zone key cardinality estimated and zone sized above it to prevent LRU eviction resets.
- Decision made explicitly on
nodelay: burst forwarded instantly (throughput) vs paced (shaping). - Kong
window_type: slidingselected where minute-boundary doubling would violate an SLA. - Kong
sync_ratetuned against the accuracy requirement; set toward 0 for hard limits. - Redis
timeoutset tight and fault-tolerant fallback confirmed so a Redis outage does not 500 the gateway. - Per-consumer scoping verified after the auth plugin, not before, so counters key on identity.
-
RateLimit-Remaining/Retry-Afterheaders confirmed present (Kong) or documented as absent (NGINX).
FAQ
Is NGINX or Kong more accurate for rate limiting across multiple nodes?
Kong is more accurate across nodes because its rate-limiting-advanced plugin keeps counters in a shared Redis backend, so every data-plane replica decrements the same counter. NGINX limit_req counters live in a per-process shared-memory zone that is not shared across separate NGINX hosts, so an N-node NGINX fleet without an external coordination layer effectively allows N times the configured limit. NGINX is accurate on a single instance; Kong with Redis is accurate across a fleet.
What is the difference between NGINX leaky bucket and Kong sliding window?
NGINX limit_req implements a leaky bucket: requests drain at a fixed rate and the burst parameter sets the bucket depth, so excess requests are either queued or rejected to smooth traffic to a steady rate. Kong’s rate-limiting-advanced offers a sliding window that counts requests over a rolling time window, which more accurately caps requests per period without the fixed-drain shaping. Leaky bucket is about smoothing throughput; sliding window is about counting against a quota. They solve related but different problems.
Can NGINX rate limit per API consumer rather than per IP?
NGINX open source keys limit_req on any variable, so you can key on a header or a JWT claim extracted into a variable, but it has no native concept of an authenticated consumer identity or per-consumer quota tiers. Kong is consumer-native: after an auth plugin identifies the consumer, the rate-limiting plugin scopes counters to that consumer automatically and supports per-consumer overrides. If you need per-plan quotas, Kong is far less work; NGINX requires you to build the identity-to-key mapping yourself.
When should I use NGINX instead of Kong for rate limiting?
Use NGINX when you already run it as your edge, the limit is coarse and IP-based, and a single instance or sticky-hashed pool covers your traffic — it is extremely fast and adds negligible latency. Use Kong when you need per-consumer quotas, accurate distributed counters across many replicas, sliding-window accuracy, or response headers that tell clients their remaining quota. Kong adds a Redis dependency and a few milliseconds of latency in exchange for accuracy and consumer awareness.
Parent: Rate Limiting & Throttling Strategies
Related
- Dynamic Rate Limiting with Redis Backends — the Redis topology and
sync_ratetuning that keep Kong’s distributed counters accurate under load. - Middleware Chains & Request Transformation — where the limiter sits in the policy chain, after auth and before transformation.
- Authentication Proxying & Token Validation — how the consumer identity that per-consumer limits key on is established.