Skip to main content

NGINX

NGINX Cookie Limit Module: Rate Limiting Guide

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

Attackers frequently exploit HTTP cookies to bypass traditional rate limiting. A single malicious IP can rotate or forge cookies to appear as multiple legitimate users, overwhelming your server while evading standard defenses. The NGINX cookie limit module (ngx_cookie_limit_req_module) solves this problem by combining cookie-aware rate limiting with Redis-backed forged cookie detection.

In this guide, you will learn how to install, configure, and deploy the NGINX cookie limit module to protect your server from cookie-based abuse.

The ngx_cookie_limit_req_module is a third-party NGINX module that provides two layers of protection against cookie-based attacks:

  1. Cookie rate limiting — Applies a leaky-bucket rate limit specifically to requests that carry cookies, similar to NGINX’s native limit_req but scoped to cookie-bearing traffic.
  2. Forged cookie detection — Uses Redis HyperLogLog to track how many unique cookie values each IP address generates. When an IP exceeds the configured threshold, it is blocked entirely.

This combination makes the NGINX cookie limit module effective against bots and scrapers that rotate session cookies to bypass per-session rate limits.

How Does It Work?

The module operates in the NGINX preaccess phase and uses Redis as a shared state store across all worker processes.

Like NGINX’s built-in limit_req, this module uses a leaky-bucket algorithm to control the rate of cookie-bearing requests. You define a rate (e.g., 30r/s) and an optional burst size. Requests that exceed the rate are either delayed or rejected.

This is the module’s unique feature. For each incoming request that carries a cookie:

  1. The module computes an MD5 hash of the cookie value.
  2. It adds the hash to a Redis HyperLogLog data structure keyed by the client’s IP address (PFADD).
  3. It checks the cardinality of the HyperLogLog (PFCOUNT) — the number of unique cookie values from that IP.
  4. If the count exceeds cookie_max, the IP is blocked for block_second seconds by setting a Redis key with a TTL.

HyperLogLog is a probabilistic data structure that uses minimal memory (approximately 12 KB per IP) while providing accurate cardinality estimates. This makes the module efficient even under high traffic loads.

Why Not Use Native NGINX Rate Limiting?

NGINX’s built-in limit_req module is excellent for general-purpose rate limiting, but it has limitations when dealing with cookie-based attacks:

Feature Native limit_req Cookie Limit Module
Rate limiting by IP Yes Yes
Rate limiting by cookie Yes (via $cookie_*) Yes
Cross-worker state Shared memory only Redis (persistent)
Forged cookie detection No Yes (HyperLogLog)
Survives NGINX restart No Yes (Redis)
Blocks IPs with too many cookies No Yes

If you only need basic per-IP rate limiting, the native limit_req directive is sufficient. However, if you face attackers who rotate cookies or forge session identifiers, the NGINX cookie limit module provides the additional protection you need.

Prerequisites

The module requires a running Redis-compatible server. Install Redis or Valkey (a Redis-compatible alternative available on newer distributions):

# RHEL 9 and older / Debian / Ubuntu
sudo dnf install redis
sudo systemctl enable --now redis

# Rocky Linux 10 / Fedora 42+
sudo dnf install valkey
sudo systemctl enable --now valkey

Verify the server is running:

redis-cli ping

You should see PONG in response.

RHEL, CentOS, AlmaLinux, Rocky Linux

Install the GetPageSpeed repository and the module package:

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-cookie-limit

Load the module by adding the following directive at the top of /etc/nginx/nginx.conf, before any http block:

load_module modules/ngx_http_cookie_limit_req_module.so;

SELinux Configuration

On RHEL-based systems with SELinux enabled (the default), NGINX is not allowed to make network connections to Redis. You must enable the httpd_can_network_connect boolean:

sudo setsebool -P httpd_can_network_connect 1

Without this, you will see redis connection error: Permission denied 127.0.0.1 in the NGINX error log.

Debian and Ubuntu

First, set up the GetPageSpeed APT repository, then install the module:

sudo apt-get update
sudo apt-get install nginx-module-cookie-limit

On Debian/Ubuntu, the package handles module loading automatically. No load_module directive is needed.

You can find detailed package information on the RPM module page or the APT module page.

Configuration Directives

The module provides four directives. All directive names and their parameters are verified against the module source code.

Defines a shared memory zone for cookie rate limiting and configures the Redis connection for forged cookie detection.

Syntax:  cookie_limit_req_zone key zone=name:size rate=rate redis=ip block_second=seconds cookie_max=number
Default: —
Context: http

Parameters:

  • key — The variable used as the rate-limiting key. Typically $http_cookie to limit by cookie value, or $binary_remote_addr to limit by IP.
  • zone=name:size — The name and size of the shared memory zone. For example, zone=cookies:10m allocates 10 MB. Each zone stores rate-limiting state in a red-black tree.
  • rate=Xr/s or rate=Xr/m — The maximum request rate. For example, rate=10r/s allows 10 requests per second. Use r/m for requests per minute.
  • redis=ip — The IP address of your Redis server. The module connects to port 6379 (hardcoded). For a local Redis instance, use redis=127.0.0.1.
  • block_second=seconds — How long (in seconds) to block an IP or cookie that triggers the limit. For example, block_second=600 blocks for 10 minutes.
  • cookie_max=number — The maximum number of unique cookie values a single IP can generate before being blocked. For example, cookie_max=10 means an IP that sends more than 10 different cookie values will be blocked.

Enables cookie rate limiting in a specific context, referencing a zone defined by cookie_limit_req_zone.

Syntax:  cookie_limit_req zone=name [burst=number] [nodelay]
Default: —
Context: http, server, location, if

Parameters:

  • zone=name — The name of a zone defined with cookie_limit_req_zone.
  • burst=number — The maximum number of excess requests to queue before rejecting. Defaults to 0 (no burst allowed). Excess requests within the burst are delayed to match the configured rate.
  • nodelay — Process burst requests immediately without delay. Without nodelay, excess requests within the burst are spaced out evenly.

Sets the severity level for log messages when the module rejects or delays requests.

Syntax:  cookie_limit_req_log_level info | notice | warn | error
Default: cookie_limit_req_log_level error;
Context: http, server, location

When a request is rejected, the module logs at the configured level. Delayed requests are logged one level below the configured level. For example, if you set cookie_limit_req_log_level notice, rejections are logged at notice and delays at info.

Sets the HTTP status code returned to clients when their requests are rejected.

Syntax:  cookie_limit_req_status code
Default: cookie_limit_req_status 503;
Context: http, server, location, if

The status code must be between 400 and 599. Common choices include:

  • 503 (Service Unavailable) — The default. Appropriate for temporary rate limiting.
  • 429 (Too Many Requests) — The standard HTTP status for rate limiting. Recommended for API endpoints.
  • 403 (Forbidden) — Useful when you want to clearly signal that access is denied.

Configuration Examples

Basic Setup

This configuration limits cookie-bearing requests to 10 per second per cookie value, with a burst of 20. IPs that generate more than 5 unique cookies are blocked for 5 minutes:

http {
    cookie_limit_req_zone $http_cookie zone=cookies:10m rate=10r/s
                          redis=127.0.0.1 block_second=300 cookie_max=5;

    server {
        listen 80;
        server_name example.com;

        cookie_limit_req zone=cookies burst=20 nodelay;
        cookie_limit_req_status 429;

        location / {
            proxy_pass http://backend;
        }
    }
}

Strict Anti-Bot Configuration

For high-security applications where cookie forgery is a significant threat, use a lower cookie_max and a longer block duration:

http {
    cookie_limit_req_zone $http_cookie zone=strict:20m rate=5r/s
                          redis=127.0.0.1 block_second=3600 cookie_max=3;

    server {
        listen 80;
        server_name secure.example.com;

        cookie_limit_req zone=strict burst=10;
        cookie_limit_req_status 403;
        cookie_limit_req_log_level warn;

        location / {
            proxy_pass http://backend;
        }
    }
}

In this example, any IP that generates more than 3 unique cookie values is blocked for one hour. The rate is limited to 5 requests per second with a burst of 10 (delayed, not immediate).

Protecting Login Endpoints

Apply cookie rate limiting specifically to authentication endpoints where cookie abuse is most common:

http {
    cookie_limit_req_zone $http_cookie zone=auth:5m rate=3r/s
                          redis=127.0.0.1 block_second=600 cookie_max=5;

    server {
        listen 80;
        server_name example.com;

        location /login {
            cookie_limit_req zone=auth burst=5 nodelay;
            cookie_limit_req_status 429;
            proxy_pass http://backend;
        }

        location / {
            proxy_pass http://backend;
        }
    }
}

Redis Configuration for Production

Securing Redis

Since the module connects to Redis on port 6379, make sure Redis is properly secured:

  1. Bind to localhost only — In /etc/redis/redis.conf or /etc/redis.conf:
bind 127.0.0.1
  1. Disable protected mode warning (if bound to localhost):
protected-mode yes
  1. Set a password (optional but recommended if Redis is on a separate host):
requirepass your_strong_password_here

Note that the module does not support Redis authentication. Keep Redis bound to localhost or use firewall rules to restrict access.

Valkey Compatibility

On newer distributions like Rocky Linux 10 and Fedora 42+, the redis package has been replaced by Valkey, a fully compatible Redis fork. The module works seamlessly with Valkey — no configuration changes are needed. Simply install valkey instead of redis and the module connects to it on the same default port 6379.

Redis Memory Management

The module uses Redis HyperLogLog structures and key-value pairs with TTLs. Memory usage depends on your traffic volume:

  • Each HyperLogLog key uses approximately 12 KB.
  • Each blocked IP/cookie key is a small string with a TTL.

For most deployments, allocating 64-128 MB to Redis is sufficient. Set a memory limit in Redis configuration:

maxmemory 128mb
maxmemory-policy allkeys-lru

Testing the Configuration

After configuring the module, verify the configuration syntax:

nginx -t

If the test passes, reload NGINX:

sudo systemctl reload nginx

Verifying the Module Is Loaded

Confirm the module file exists:

ls /usr/lib64/nginx/modules/ngx_http_cookie_limit_req_module.so

Then test that NGINX accepts the configuration with module-specific directives — a successful nginx -t proves the module is loaded and working.

Testing Rate Limiting

The module exempts requests from 127.0.0.1 from rate-limit enforcement (by design, to avoid blocking local health checks). To test rate limiting, use the server’s actual IP address or hostname:

for i in $(seq 1 50); do
    curl -s -o /dev/null -w "%{http_code}\n" \
         -b "session=test_cookie_value" \
         http://your-server-ip/
done

You should see 200 responses initially, followed by 429 (or your configured status code) once the rate limit is exceeded. With burst=20 nodelay, the first 21 requests pass and the rest are rejected.

Forged cookie detection (cookie_max) applies to all client IPs including localhost. Simulate an attacker rotating cookies:

for i in $(seq 1 20); do
    curl -s -o /dev/null -w "Cookie $i: %{http_code}\n" \
         -b "session=forged_cookie_$i" \
         http://localhost/
done

After the number of unique cookies from your IP exceeds cookie_max, subsequent requests should return the configured error status code.

Monitoring Redis State

Inspect the Redis keys created by the module:

redis-cli keys '*'

You should see HyperLogLog keys prefixed with the client IP (e.g., [192.168.1.100]Independent_IP) and any blocked IP/cookie keys.

To check how many unique cookies an IP has generated:

redis-cli PFCOUNT "[192.168.1.100]Independent_IP"

Performance Considerations

Shared Memory Zone Sizing

The shared memory zone stores rate-limiting state in a red-black tree. Each entry uses approximately 128 bytes. A 10 MB zone can hold around 80,000 entries, which is sufficient for most deployments.

If you see errors like could not allocate node in your NGINX error log, increase the zone size:

cookie_limit_req_zone $http_cookie zone=cookies:20m rate=10r/s
                      redis=127.0.0.1 block_second=300 cookie_max=5;

Redis Latency

Each request with a cookie triggers multiple Redis commands (PFADD, PFCOUNT, and potentially GET/SETEX). The module uses a 1.5-second connection timeout and connects on port 6379.

To minimize latency:

  • Run Redis on the same host as NGINX (use redis=127.0.0.1).
  • Monitor Redis latency with redis-cli --latency.

Graceful Degradation

If the Redis connection fails, the module logs an error and allows the request through (NGX_DECLINED). This means your server continues to function even if Redis is temporarily unavailable — rate limiting simply becomes inactive until the connection is restored.

Security Best Practices

Choose Appropriate Thresholds

  • cookie_max — Set this based on your application’s normal behavior. A typical web application generates 1-3 unique cookie values per user session (session ID, CSRF token, preferences). A cookie_max of 5-10 provides a reasonable margin.
  • block_second — Start with 300 seconds (5 minutes) and increase if attacks persist. For persistent abusers, 3600 seconds (1 hour) is appropriate.
  • rate — Analyze your normal traffic patterns. A rate of 10-30 r/s per cookie value is typical for web applications.

Combine with Other Security Modules

The module works well alongside other NGINX security tools:

  • Testcookie — Issues challenge cookies to verify that clients support JavaScript, filtering out simple bots before they reach the cookie rate limiter.
  • Cookie Flag — Sets HttpOnly, Secure, and SameSite attributes on cookies to prevent client-side cookie theft.
  • JavaScript Challenge — Another bot detection approach that complements cookie rate limiting.
  • Sysguard — Provides system-level protection against server overload, adding a second layer of defense when rate limiting alone is insufficient.

Monitor Your Logs

Watch for rate-limiting and blocking messages in the NGINX error log:

tail -f /var/log/nginx/error.log | grep -i "cookie"

The module logs messages like:

  • limiting cookie requests, excess: X.XXX by zone "name" lock=HASH — when a specific cookie is rate-limited.
  • The number of cookies generated by the host has reached the limit: by zone "name" Host=IP cookie_max=N — when an IP exceeds the cookie_max threshold.

Troubleshooting

“Unknown directive” Error

If nginx -t reports an unknown directive error, the module is not loaded. Verify:

  1. The module file exists at /usr/lib64/nginx/modules/ngx_http_cookie_limit_req_module.so.
  2. The load_module directive is at the top of nginx.conf, outside any block.
  3. The module version matches your NGINX version.

Redis Connection Errors

If you see redis connection error in the NGINX error log:

  1. Verify Redis is running: systemctl status redis (or systemctl status valkey on Rocky Linux 10+).
  2. Check Redis is listening on 127.0.0.1:6379: redis-cli ping.
  3. Check firewall rules if Redis is on a different host.
  4. On RHEL-based systems, ensure SELinux allows network connections: sudo setsebool -P httpd_can_network_connect 1.

Remember that the module connects to port 6379 only — this port number is hardcoded and cannot be changed via configuration.

Requests Not Being Limited

If rate limiting appears inactive:

  1. Confirm the request carries a Cookie header — the module only processes requests with cookies.
  2. Check that the cookie_limit_req directive is in the correct context (http, server, or location).
  3. Verify Redis is accepting writes: redis-cli SET test ok && redis-cli GET test.
  4. Check that cookie_limit_req_zone is in the http context, not inside a server or location block.
  5. Make sure you are not testing from 127.0.0.1 — the module exempts localhost from rate-limit enforcement. Use the server’s real IP address for testing.

High Redis Memory Usage

If Redis memory grows unexpectedly:

  1. Check for leaked keys without TTLs: redis-cli --scan | head -20.
  2. HyperLogLog keys ([IP]Independent_IP) do not expire automatically. They are removed only when the associated IP is blocked (the module calls DEL on the HyperLogLog after blocking).
  3. Consider adding a maxmemory policy to Redis as described in the Redis Configuration section.

Module Limitations

Before deploying the module, be aware of its current limitations:

  • Redis port is hardcoded — The module always connects to port 6379. You cannot specify a custom port.
  • No Redis authentication — The module does not support AUTH commands. Redis must be accessible without a password or via network restrictions.
  • No Redis Sentinel or Cluster support — The module connects to a single Redis instance. For high availability, use an external Redis proxy.
  • Global Redis connection — The module uses a single global Redis connection, which may become a bottleneck under very high concurrency.
  • Localhost rate-limit exemption — Requests from 127.0.0.1 bypass rate-limit enforcement (the cookie forging detection via cookie_max still applies). This prevents accidental blocking of local health checks, but means you must test rate limiting using the server’s real IP.

Conclusion

The NGINX cookie limit module provides effective protection against cookie-based attacks by combining traditional rate limiting with Redis-backed forged cookie detection. It is particularly useful for servers that face automated attacks where bots rotate session cookies to bypass standard rate limits.

For straightforward rate limiting without Redis, consider NGINX’s built-in rate limiting. For more advanced bot protection, combine the cookie limit module with testcookie or a JavaScript challenge.

The module source code is available on GitHub.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.