Nginx Rate Limiting on VPS
Rate limiting is a technique to restrict the number of requests a single IP address can make per second, helping protect against brute force login attempts, API abuse, and reducing the impact of lightweight DDoS attacks. Nginx has built-in rate limiting that is easy to configure and highly efficient without requiring additional plugins or modules.
Fundamentals: limit_req_zone
# Configure in nginx.conf or /etc/nginx/conf.d/
# In the http {} block:
# Create a zone named "one" with 10MB size, allowing 10 requests/second per IP
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
# Zone for login endpoint (stricter)
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
Apply Rate Limiting in Server Block
server {
listen 443 ssl;
server_name yourdomain.com;
# Entire site: 10 req/s, with burst of 20
limit_req zone=one burst=20 nodelay;
# Login page: stricter limits
location /wp-login.php {
limit_req zone=login burst=5 nodelay;
# If exceeded: 429 Too Many Requests
limit_req_status 429;
proxy_pass http://localhost:8080;
}
# API: no burst allowed (strict)
location /api/ {
limit_req zone=one burst=5;
proxy_pass http://localhost:3000;
}
}
Rate Limit Connections (limit_conn)
# http {} block:
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
# server {} block:
# Limit a single IP to maximum 20 concurrent connections
limit_conn conn_limit 20;
Whitelist IP Addresses for Admin
# Use geo module to exempt certain IPs
geo $limit {
default 1;
127.0.0.1 0;
203.0.113.0/24 0; # office IP range
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=protected:10m rate=10r/s;
# Use in server block:
location /admin/ {
limit_req zone=protected burst=20;
}
Log Rejected Requests
# In nginx.conf:
limit_req_log_level warn; # log level (warn/error/crit/alert)
# View logs:
sudo tail -f /var/log/nginx/error.log | grep limiting
ab -n 100 -c 20 https://yourdomain.com/api/ (Apache Benchmark) to see when 429 responses appear. Adjust burst values to match your actual traffic patterns.
Choose Rate Values Suitable for Your Use Case
Setting too low risks blocking legitimate users with 503 errors; setting too high fails to block abusers. Use this table as a starting point, then fine-tune based on actual logs:
| Endpoint Type | Recommended Rate | Burst |
|---|---|---|
| General web pages | 30r/s | 50 |
| API (read) | 10r/s | 20 |
| Login / registration pages | 1r/s | 5 nodelay |
| Email / OTP sending | 1r/m | 3 |
burst=N nodelay on login pages to allow short traffic spikes (like a user mistyping and retrying) while still blocking sustained brute-force attempts — this differs from delay, which queues requests rather than rejecting them immediately.Verify Rate Limiting is Working Correctly
# Check config has no syntax errors
sudo nginx -t
# Reload nginx
sudo systemctl reload nginx
# Test burst with a curl loop
for i in {1..20}; do curl -s -o /dev/null -w "%{http_code}\n" https://yourdomain.com/api/test; done
# Check for 429 in logs
sudo grep "limiting requests" /var/log/nginx/error.log | tail -20
error_page 429 /rate-limit.html;
Common Issues & Troubleshooting
Legitimate users getting blocked (False positives)
Burst values are too low or rate is too low. Increase burst first, or check if the IP is behind a proxy or CDN:
# If traffic flows through Cloudflare, use CF-Connecting-IP instead of remote_addr
set_real_ip_from 103.21.244.0/22; # Cloudflare IP range
real_ip_header CF-Connecting-IP;
limit_req_zone $realip_remote_addr zone=one:10m rate=10r/s;
Error: "zone size is too small"
A 10m zone can handle approximately 160,000 unique IPs. If traffic is larger, increase zone size:
limit_req_zone $binary_remote_addr zone=one:50m rate=10r/s;
Rate Limiting not working with upstream via proxy_pass
limit_req must come before proxy_pass in the location block, or be at server block level:
location /api/ {
limit_req zone=one burst=10 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
Frequently Asked Questions
How do rate limiting and fail2ban differ?
Nginx rate limiting works in real-time, immediately rejecting requests once the limit is exceeded. fail2ban analyzes logs after the fact and bans IPs using iptables. Both work well together: rate limiting handles the initial load, while fail2ban manages IPs that keep attempting after being blocked.
What does nodelay do?
By default, Nginx delays requests that exceed the rate but are within the burst, allowing them through slowly. The nodelay flag forces requests within burst to be processed immediately without delay, while still tracking the counter. This is ideal for APIs requiring low latency.
Can rate limiting be applied per user instead of IP?
Yes, use $http_authorization or cookie values instead of IP: limit_req_zone $http_x_api_key zone=api:10m rate=100r/s. This is useful for APIs needing per-API-key rate limiting.