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.

Counter locality: NGINX per-instance zones vs Kong shared Redis On the left, three NGINX instances each hold their own local shared-memory counter, so the effective limit is multiplied by the node count. On the right, three Kong data-plane nodes all decrement one shared Redis counter, so the limit is enforced accurately across the fleet. NGINX — local zones Kong — shared Redis NGINX node 1 zone: 100 r/s NGINX node 2 zone: 100 r/s NGINX node 3 zone: 100 r/s effective 300 r/s Kong DP 1 Kong DP 2 Kong DP 3 Redis 100 r/s local counters multiply by node count; a shared counter enforces one global limit

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 10m zone 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.
  • nodelay defeats shaping. burst=200 nodelay forwards all 200 instantly; without nodelay NGINX paces them at the drain rate. If you added burst to protect a fragile upstream, nodelay sends exactly the spike you were guarding against.
  • Kong sync_rate drift. A high sync_rate under a traffic spike lets each node overshoot before it flushes to Redis. If your SLA is hard, lower sync_rate toward 0 and accept the Redis round-trip cost.
  • Kong fixed-window boundary burst. Using the basic rate-limiting plugin with policy: redis still uses a fixed window — the minute-boundary doubling applies. Switch to rate-limiting-advanced with window_type: sliding when 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 timeout and 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: sliding selected where minute-boundary doubling would violate an SLA.
  • Kong sync_rate tuned against the accuracy requirement; set toward 0 for hard limits.
  • Redis timeout set 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-After headers 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