Nginx limit_conn: Per-IP Concurrent Connection Limits
Per-IP high-concurrency scraping and download-tool abuse will not be stopped by rate limits alone—connection limits are the fix. Covers limit_conn_zone shared memory, per-IP and per-server caps, location stacking, custom 429, pairing with limit_req, plus HTTP/2 false positives and CDN real-IP troubleshooting.
Scope
Sites hit by per-IP high-concurrency flooding, image/static grabbers, or a single client saturating workers share one trait: one source IP holds many connections at once. limit_conn is Nginx's built-in concurrent-connection limiter. Pair it with limit_req (rate) to cap connections per IP and per vhost at a given instant. This article configures Nginx limit_conn against per-IP connection abuse.
Prerequisites
- Nginx 1.12+ (
limit_connis core; no extra compile) - root or sudo; can edit the main config
- Known healthy concurrency: read access.log, upper bound of simultaneous connections for one normal user
- If the backend is PHP-FPM, know fastcgi connection behavior; do not clip long-lived connections by accident
How it works
limit_conn counts in shared memory. limit_conn_zone carves a zone keyed by a variable (usually client IP $binary_remote_addr). New connection: +1. Close: −1. When the key's live count exceeds the limit_conn threshold, Nginx returns 503 Service Unavailable (override with limit_conn_status).
Difference: limit_req caps request rate (N/sec). limit_conn caps concurrency (how many connections hang at once). Download tools and crawlers often open 10+ connections and pull large files. Rate may look fine; concurrency is far above a browser (typically 6 per origin). Use both.
Steps
1. Define shared-memory zones
In the http block. Zone name perip; 10m holds ~160k IP connection states:
http {
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
}
Key must be $binary_remote_addr, not $remote_addr. Four-byte binary form uses less memory and counts more accurately.
2. Apply in server or location
server {
listen 80;
server_name example.com;
# Site-wide: 20 connections per IP
limit_conn perip 20;
# Whole vhost: 500 connections across all IPs
limit_conn perserver 500;
# Over limit → 429, easier for frontends and monitoring
limit_conn_status 429;
# Tighter on the download tree
location /download/ {
limit_conn perip 2;
limit_rate 512k;
}
# Static assets at normal browser level
location ~* \\.(css|js|png|jpg|webp)$ {
limit_conn perip 10;
}
}
location limit_conn inherits and stacks with server. Requests under /download/ hit both perip 20 and perip 2; 2 wins.
3. Pair with rate limit (optional)
Concurrency limits do not stop a single connection hammering requests. Stack limit_req:
http {
limit_req_zone $binary_remote_addr zone=reqperip:10m rate=10r/s;
server {
limit_req zone=reqperip burst=20 nodelay;
}
}
4. Reload
nginx -t && systemctl reload nginx
Verify
Simulate concurrency with ab or curl; over-limit should be 429:
# 30 concurrent, 60 total; watch Non-2xx responses
ab -n 60 -c 30 http://example.com/download/test.zip
# Or keep connections with parallel curl
for i in $(seq 1 30); do curl -s -o /dev/null -w "%{http_code}\n" http://example.com/download/test.zip & done | sort | uniq -c
Expect ~2 requests 200, the rest 429. Confirm in error log:
tail -f /var/log/nginx/error.log
# similar: limiting connections by zone "perip", client: 1.2.3.4, ...
FAQ
FAQ 1: Real users clipped; page assets incomplete
Modern browsers open ~6 connections per origin. With extra third-party hosts or non-HTTP/2, a cap under 20 is usually safe. Debug: check restricted URIs in error.log. If they are normal page assets, raise that location's limit_conn or exclude it. HTTP/2 reuses one TCP connection; limit_conn counts TCP, so HTTP/2 users are naturally less affected. If false positives cluster on HTTP/2 users, check http2 on and whether the connection is counted more than once.
FAQ 2: CDN or reverse proxy: every request looks like one IP
CDN, LB, or Nginx proxy in front → $binary_remote_addr is the proxy node. All users share one counter; mass 429. Key on the real-IP header instead:
limit_conn_zone $http_x_forwarded_for zone=perip:10m;
Cleaner: ngx_http_realip_module, set_real_ip_from for trusted CDN CIDRs, then keep $binary_remote_addr. Logs and limits both see the real client IP.
FAQ 3: 503 or 429?
Nginx default is 503. 429 Too Many Requests is the right semantic, and CDNs/clients back off per spec. Set limit_conn_status 429 explicitly.
Summary
limit_conn is concurrency resource control. With limit_req (rate) and limit_rate (bandwidth) it is Nginx's three-layer traffic defense. Keys: $binary_remote_addr, measure healthy waterline then set caps, tighten download trees separately, handle real IP on CDN. Smaller is not always better. Start at 20 per IP, then tighten from error.log hits.