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.
What Is the NGINX Cookie Limit Module?
The ngx_cookie_limit_req_module is a third-party NGINX module that provides two layers of protection against cookie-based attacks:
- Cookie rate limiting — Applies a leaky-bucket rate limit specifically to requests that carry cookies, similar to NGINX’s native
limit_reqbut scoped to cookie-bearing traffic. - 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.
Cookie Rate Limiting (Leaky Bucket)
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.
Forged Cookie Detection (HyperLogLog)
This is the module’s unique feature. For each incoming request that carries a cookie:
- The module computes an MD5 hash of the cookie value.
- It adds the hash to a Redis HyperLogLog data structure keyed by the client’s IP address (
PFADD). - It checks the cardinality of the HyperLogLog (
PFCOUNT) — the number of unique cookie values from that IP. - If the count exceeds
cookie_max, the IP is blocked forblock_secondseconds 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.
Installing the NGINX Cookie Limit Module
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_moduledirective 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.
cookie_limit_req_zone
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_cookieto limit by cookie value, or$binary_remote_addrto limit by IP.zone=name:size— The name and size of the shared memory zone. For example,zone=cookies:10mallocates 10 MB. Each zone stores rate-limiting state in a red-black tree.rate=Xr/sorrate=Xr/m— The maximum request rate. For example,rate=10r/sallows 10 requests per second. User/mfor requests per minute.redis=ip— The IP address of your Redis server. The module connects to port 6379 (hardcoded). For a local Redis instance, useredis=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=600blocks 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=10means an IP that sends more than 10 different cookie values will be blocked.
cookie_limit_req
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 withcookie_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. Withoutnodelay, excess requests within the burst are spaced out evenly.
cookie_limit_req_log_level
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.
cookie_limit_req_status
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:
- Bind to localhost only — In
/etc/redis/redis.confor/etc/redis.conf:
bind 127.0.0.1
- Disable protected mode warning (if bound to localhost):
protected-mode yes
- 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.
Testing Forged Cookie Detection
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). Acookie_maxof 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, andSameSiteattributes 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 thecookie_maxthreshold.
Troubleshooting
“Unknown directive” Error
If nginx -t reports an unknown directive error, the module is not loaded. Verify:
- The module file exists at
/usr/lib64/nginx/modules/ngx_http_cookie_limit_req_module.so. - The
load_moduledirective is at the top ofnginx.conf, outside any block. - The module version matches your NGINX version.
Redis Connection Errors
If you see redis connection error in the NGINX error log:
- Verify Redis is running:
systemctl status redis(orsystemctl status valkeyon Rocky Linux 10+). - Check Redis is listening on
127.0.0.1:6379:redis-cli ping. - Check firewall rules if Redis is on a different host.
- 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:
- Confirm the request carries a
Cookieheader — the module only processes requests with cookies. - Check that the
cookie_limit_reqdirective is in the correct context (http, server, or location). - Verify Redis is accepting writes:
redis-cli SET test ok && redis-cli GET test. - Check that
cookie_limit_req_zoneis in thehttpcontext, not inside aserverorlocationblock. - 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:
- Check for leaked keys without TTLs:
redis-cli --scan | head -20. - HyperLogLog keys (
[IP]Independent_IP) do not expire automatically. They are removed only when the associated IP is blocked (the module callsDELon the HyperLogLog after blocking). - Consider adding a
maxmemorypolicy 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
AUTHcommands. 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.1bypass rate-limit enforcement (the cookie forging detection viacookie_maxstill 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.

