Nginx limit_req in practice: complete CC-attack defense config
Complete Nginx limit_req CC-attack defense: rate-limit policy, leaky-bucket internals, whitelist, and load-test verification.
Scope
When a web server is hit by a CC (Challenge Collapsar) attack, the attacker floods it with high-frequency HTTP requests, exhausting the connection pool and bandwidth so legitimate users cannot connect. This article shows how to build a production-ready rate-limit defense with Nginx's native `limit_req` module. Applies to standalone Nginx or Nginx as a reverse proxy.
Prerequisites
- OS: CentOS 7+ / Ubuntu 20.04+ / Debian 11+
- Nginx: ≥ 1.18.0 (compiled with `--with-http_limit_req_module`; included by default in mainstream distros)
- Privileges: root or sudo
- `ab` (Apache Bench) or `wrk` installed for load testing
Run this to confirm the module is present:
nginx -V 2>&1 | grep -o 'http_limit_req_module'
Output `http_limit_req_module` means the module is enabled.
Internals
The `limit_req` module rate-limits with the leaky-bucket algorithm. Core mechanism:
- Rate-limit key: client IP (`$binary_remote_addr`) or another variable
- Rate: e.g. `10r/s` allows at most 10 requests per second
- Burst buffer: `burst` sizes the short-burst queue
- No-delay: `nodelay` processes burst requests immediately instead of queueing
When the rate exceeds the threshold, Nginx returns 503 Service Unavailable and drops malicious requests immediately, protecting the backend.
Steps
Step 1: Define the rate-limit zone
Edit `/etc/nginx/nginx.conf`. Add the zone inside the `http` block:
http {
# ... other configuration ...
# Limit each IP to at most 10 requests per second; 10MB buffer
limit_req_zone $binary_remote_addr zone=cc_defense:10m rate=10r/s;
# Optional: rate-limit by request URI (for specific endpoints)
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s;
}
Parameters:
- `$binary_remote_addr`: binary client IP; cheaper than `$remote_addr`
- `zone=cc_defense:10m`: 10MB shared memory, ~160k IP records
- `rate=10r/s`: 10 requests per second (one every 100ms on average)
Step 2: Apply the rule in server/location
Edit the site config (e.g. `/etc/nginx/conf.d/site.conf`):
server {
listen 443 ssl http2;
server_name helixcyber.ai;
# SSL certificate configuration omitted
# Global rate limit (all requests)
location / {
limit_req zone=cc_defense burst=20 nodelay;
limit_req_status 503;
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Rate-limit API endpoints separately (stricter)
location /wp-json/ {
limit_req zone=api_limit burst=10 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Do not rate-limit static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
# Do not set limit_req
proxy_pass http://127.0.0.1:8080;
}
}
Key parameters:
- `burst=20`: queue up to 20 burst requests
- `nodelay`: process burst immediately, no rate-based delay
- `limit_req_status 503`: return 503 when limited (503 is also the default)
Step 3: IP whitelist (trusted sources skip the limit)
Use `geo` to define a whitelist so CDN origin IPs and monitoring probes are not limited:
http {
# Define trusted IP list
geo $limit_ip {
default 1; # Rate-limit by default
127.0.0.1 0; # Do not rate-limit localhost
10.0.0.0/8 0; # Do not rate-limit internal network
# CDNoriginIP
123.45.67.0/24 0;
}
# Control the rate-limit key with a variable: trusted IPs use an empty value to skip the limit
map $limit_ip $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=cc_defense:10m rate=10r/s;
}
Whitelisted IPs then skip the rate-limit rule.
Step 4: Custom error page
Serve a friendlier 503 page:
server {
# ... other configuration ...
error_page 503 = @cc_blocked;
location @cc_blocked {
default_type application/json;
return 503 '{"code":503,"msg":"Too many requests, please try again later"}';
}
}
Step 5: Test and reload
# syntax check
nginx -t
# graceful reload
nginx -s reload
Verification
Simulate high-frequency requests with `ab` to confirm the limit:
# Send 100 requests, concurrency 20
ab -n 100 -c 20 https://helixcyber.ai/
# Expected:
# Complete requests: 100
# Failed requests: 80 ← requests exceeding the rate are rejected
# Non-2xx responses: 80 ← return 503 status code
Low-frequency legitimate requests should all return 200:
# 1 request per second, 10 total
for i in $(seq 1 10); do
curl -s -o /dev/null -w "%{http_code}\n" https://helixcyber.ai/
sleep 1
done
# Expected: 10 lines of 200
Check the Nginx error log for limit events:
tail -f /var/log/nginx/error.log | grep "limiting requests"
# Expected example:
# limiting requests, excess: 20.000 by zone "cc_defense", client: 1.2.3.4 ...
FAQ
Q1: How to choose burst and nodelay?
`burst` sizes the burst queue. Without `nodelay`, burst requests are processed at the configured rate (they wait in queue). With `nodelay`, burst requests are processed immediately but still count against the quota. For CC defense, use `nodelay` so legitimate users do not wait in queue.
Q2: Site sits behind a CDN; every request IP is the CDN origin. How to rate-limit?
Extract the real client IP from `X-Forwarded-For` or `X-Real-IP`. Add this in the `http` block:
set_real_ip_from 123.45.67.0/24; # CDN origin IP range
real_ip_header X-Forwarded-For;
real_ip_recursive on;
`$binary_remote_addr` is then replaced with the real client IP, and the limit applies per real IP.
Q3: Rate limit is too strict and blocks legitimate users. What then?
Tune the policy: raise `rate` and `burst`; skip limits on static paths; return 429 instead of 503 (better HTTP semantics); combine with `limit_conn` (concurrent connection limit) instead of relying on request rate alone.
Takeaway
Key points:
- `limit_req_zone` defines the zone and rate; `limit_req` applies it in a location
- `burst + nodelay` balances flexibility and protection
- `geo + map` whitelist exemption avoids limiting CDN and internal nets
- Behind a CDN, use `set_real_ip_from` to extract the real IP
- Combine with `limit_conn` (concurrency) and WAF rules for layered defense
Further reading: `limit_conn` concurrent-connection limits, Nginx Lua dynamic rate limiting, and Fail2ban auto-ban of malicious IPs for a fuller CC-defense stack.