← News
/PLAYBOOK · 2026-09-10

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

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.