How to Configure Nginx Rate Limiting and Connection Throttling on RHEL 7
Uncontrolled traffic is one of the most common threats to web server stability. Whether the source is a poorly written client polling too aggressively, a scraper harvesting your content, or a low-volume denial-of-service attack, allowing unlimited requests to reach your application or upstream servers will eventually exhaust resources and degrade service for legitimate users. Nginx provides two complementary built-in mechanisms for controlling traffic: rate limiting via the ngx_http_limit_req_module, which caps how many requests a client can make per unit of time, and connection throttling via the ngx_http_limit_conn_module, which limits the number of simultaneous open connections from a single source. Both modules are compiled into Nginx by default on RHEL 7. This tutorial covers configuring both, returning proper HTTP 429 responses, testing with Apache Bench and curl, and logging throttled requests for monitoring.
Prerequisites
- RHEL 7 server with
sudoaccess - Nginx installed and running:
sudo yum install -y nginx(via EPEL or nginx.org repo) - Apache Bench for load testing:
sudo yum install -y httpd-tools - Basic familiarity with editing
/etc/nginx/nginx.conf
Step 1: Understanding limit_req_zone — The Request Rate Shared Memory Zone
Before you can apply rate limiting to a location block, you must define a shared memory zone in the http context. This zone stores the state (last access time and token bucket counter) for each tracked key. The key is usually the client’s IP address ($binary_remote_addr).
# /etc/nginx/nginx.conf — http block
http {
# Define a zone named "req_limit" tracking by client IP
# Zone size: 10m (10 megabytes — stores ~160,000 IP addresses)
# Rate: 10 requests per second per IP
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=10r/s;
# A tighter zone for login/auth endpoints
limit_req_zone $binary_remote_addr zone=login_limit:5m rate=3r/m;
# ...
}
The rate value supports r/s (requests per second) and r/m (requests per minute). The zone size determines how many unique client IPs can be tracked simultaneously; 1 MB holds approximately 16,000 entries for $binary_remote_addr.
Step 2: Apply limit_req to Location Blocks
Once the zone is defined, apply it inside a server or location block with the limit_req directive:
server {
listen 80;
server_name example.com;
# General rate limiting for all requests to this site
location / {
limit_req zone=req_limit burst=20 nodelay;
root /usr/share/nginx/html;
index index.html;
}
# Tight rate limiting for the login endpoint
location /login {
limit_req zone=login_limit burst=5 nodelay;
proxy_pass http://localhost:8080;
}
# API endpoint — allow small burst, then enforce queue
location /api/ {
limit_req zone=req_limit burst=10;
proxy_pass http://localhost:3000;
}
}
Understanding burst and nodelay
- burst=20: Allows up to 20 requests to be queued above the base rate before any are rejected. Without
nodelay, queued requests are delayed and served at the defined rate. - nodelay: Serves burst requests immediately rather than queuing them with artificial delay. Excess requests beyond the burst are still rejected. Use this when you want to permit short spikes without adding latency.
- Omitting both
burstandnodelaymeans any request that arrives while the token bucket is empty receives a 503 immediately — the strictest option.
Step 3: Return HTTP 429 Instead of 503
By default, Nginx returns 503 Service Unavailable when a request is rate-limited. The more semantically correct status code is 429 Too Many Requests (RFC 6585). Change this with the limit_req_status directive:
http {
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login_limit:5m rate=3r/m;
# Return 429 instead of 503 when rate limit is exceeded
limit_req_status 429;
# Custom error page for 429
server {
error_page 429 /429.html;
location = /429.html {
root /usr/share/nginx/html;
internal;
}
}
}
Create the custom error page:
sudo tee /usr/share/nginx/html/429.html << 'EOF'
<!DOCTYPE html>
<html>
<head><title>429 Too Many Requests</title></head>
<body>
<h1>Too Many Requests</h1>
<p>You have sent too many requests in a short period. Please wait and try again.</p>
</body>
</html>
EOF
Step 4: Configure limit_conn_zone for Connection Throttling
Rate limiting controls the frequency of requests. Connection throttling, handled by ngx_http_limit_conn_module, limits the number of simultaneous open connections. This is useful for preventing a single client from opening hundreds of parallel connections to exhaust Nginx worker connection slots.
http {
# Zone tracking simultaneous connections per IP
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
# Return 429 for connection limit excess too
limit_conn_status 429;
server {
listen 80;
server_name example.com;
location / {
# Allow at most 10 simultaneous connections per client IP
limit_conn conn_limit 10;
# Combine with rate limiting
limit_req zone=req_limit burst=20 nodelay;
root /usr/share/nginx/html;
}
# Stricter limits for download endpoints
location /downloads/ {
limit_conn conn_limit 2;
limit_req zone=req_limit burst=5 nodelay;
root /var/www;
}
}
}
Step 5: Logging Throttled Requests
By default, Nginx logs rate-limited and connection-limited requests in the error log at the error level. Change this to warn to reduce noise, and add the status code to your access log format so you can count 429 responses easily:
http {
# Set log level for rate-limit rejections (error, warn, notice, info)
limit_req_log_level warn;
limit_conn_log_level warn;
# Custom log format that includes the upstream and status
log_format main_ext '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time';
access_log /var/log/nginx/access.log main_ext;
error_log /var/log/nginx/error.log warn;
}
To monitor 429 responses in real time:
# Count 429 responses per minute from the access log
sudo tail -f /var/log/nginx/access.log | grep " 429 "
# Error log entries look like:
# 2026/05/17 10:00:01 [warn] 12345#0: *1 limiting requests, excess: 0.540 ...
sudo tail -f /var/log/nginx/error.log | grep "limiting"
Step 6: Testing Rate Limits with Apache Bench
Apache Bench (ab) is the fastest way to confirm your rate limits are working. It sends a configurable number of concurrent requests to a URL.
# Reload Nginx to apply configuration changes
sudo nginx -t && sudo systemctl reload nginx
# Send 100 requests with 20 concurrent connections
ab -n 100 -c 20 http://localhost/
# Look at the Non-2xx responses in the output — these are your 429s
# Example output excerpt:
# Complete requests: 100
# Failed requests: 72
# Non-2xx responses: 72
# More targeted test against the login endpoint (rate: 3r/m)
ab -n 20 -c 5 http://localhost/login
# Expect most requests to return 429 since 20 >> 3/min allowance
Step 7: Testing with a curl Loop
A simple bash loop using curl lets you see the HTTP status codes for each individual request:
# Send 30 rapid requests, print only the HTTP status code for each
for i in $(seq 1 30); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/)
echo "Request $i: HTTP $STATUS"
done
# Expected output after the initial burst:
# Request 1: HTTP 200
# Request 2: HTTP 200
# ...
# Request 21: HTTP 429
# Request 22: HTTP 429
Step 8: Whitelisting Trusted IPs from Rate Limiting
Use a geo block to exclude trusted internal IP addresses from rate limiting by mapping them to a special key that bypasses zone enforcement:
http {
# Map client IP to a rate-limit key:
# Trusted IPs get an empty string key (no zone tracking)
# All others use their binary IP
geo $limit_key {
default $binary_remote_addr;
127.0.0.1 "";
192.168.1.0/24 "";
10.0.0.0/8 "";
}
# Use $limit_key instead of $binary_remote_addr in the zone definition
limit_req_zone $limit_key zone=req_limit:10m rate=10r/s;
limit_conn_zone $limit_key zone=conn_limit:10m;
}
When $limit_key is an empty string, Nginx skips zone tracking entirely for that client — effectively whitelisting it.
Complete nginx.conf Rate Limiting Example
http {
geo $limit_key {
default $binary_remote_addr;
127.0.0.1 "";
}
limit_req_zone $limit_key zone=req_limit:10m rate=10r/s;
limit_req_zone $limit_key zone=login_limit:5m rate=3r/m;
limit_conn_zone $limit_key zone=conn_limit:10m;
limit_req_status 429;
limit_conn_status 429;
limit_req_log_level warn;
limit_conn_log_level warn;
server {
listen 80;
server_name example.com;
error_page 429 /429.html;
location = /429.html { root /usr/share/nginx/html; internal; }
location / {
limit_req zone=req_limit burst=20 nodelay;
limit_conn conn_limit 10;
root /usr/share/nginx/html;
}
location /login {
limit_req zone=login_limit burst=5 nodelay;
limit_conn conn_limit 3;
proxy_pass http://localhost:8080;
}
}
}
sudo nginx -t
sudo systemctl reload nginx
Rate limiting and connection throttling are among the most cost-effective hardening measures available in Nginx. They require no external dependencies, no application changes, and add negligible overhead to request processing. The combination of limit_req for per-second/minute frequency enforcement and limit_conn for simultaneous connection caps covers both slow-drip attacks and burst-oriented abuse patterns. Returning 429 with a clear error page, logging at the warn level, and whitelisting internal monitoring addresses ensures that your rate limiting configuration is production-ready and observable from day one.