Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

7.3 Connections and Bandwidth: limit_conn, limit_rate

Requests aren't the only thing clients spend. Some spend connections; some spend bytes. Two directives cap each — measured both ways, with receipts.

Levels: Intermediate ~30 min read

Intermediate

What you will learn

  1. limit_conn — concurrency caps per key, and why it's the right tool against slowloris-shaped abuse.

  2. The measured demo: three connections, cap of two, live 503.

  3. limit_rate — bandwidth throttling per connection, measured against a real download.

  4. The composition math: limit_rate_after, limit_conn + limit_req, and per-location scoping.

  5. What these directives can't do — and the Layer-4 truth (Module 10 preview).


1. limit_conn — capping concurrency

nginx
limit_conn_zone $binary_remote_addr zone=conns:10m;    # creator (http)
location /conn/ {
    limit_conn  conns 2;         # consumer: max 2 SIMULTANEOUS per key
    limit_conn_status 503;       # (or 429 — see §4)
    …
}

Same creator/consumer shape as limit_req, but the counted thing is connections currently in flight through this location, per key. Request finishes → slot frees. It's not about rate at all — a client may make 100 requests/second through 1 connection and never trip it.

The workload it defends: long-held, concurrent connections — progress-bar downloads, websocket feeds, and the hostile versions (slowloris hold-open, connection-flood scrapers). The condition, manufactured honestly: two concurrent requests to the slow backend (2 s each), then a third:

bash
$ curl …/conn/ &      # occupies slot 1 (slow backend, ~2s)
$ curl …/conn/ &      # occupies slot 2
$ sleep 0.5
$ curl -w '%{http_code}\n' …/conn/
503                     # limit_conn 2: the third concurrent connection refused

(Background jobs finish; slots free; the next request succeeds — try it after the wait.)

Two facts that shape real deployments:

  • The count is per-zone, enforced where a consumer lives — a limit_conn in /downloads/ doesn't police /api/. To cap a client fleet-wide, put the consumer at http level (inherited everywhere, Lesson 2.5's ladder).

  • Keys compose with identity exactly like Lesson 7.2: per-IP default, per-token for APIs, per- $arg_key where trust allows.

2. limit_rate — capping bytes

nginx
location /speed/ {
    limit_rate  128k;       # 131072 bytes/sec, PER CONNECTION
}

Serving the 256 KB fixture through that location, measured:

bash
$ curl -o /dev/null -w 'speed=%{speed_download} time=%{time_total}s\n' \
       -H 'Host: edge.example.test' http://127.0.0.1:8700/speed/256k.bin
speed=131071 time=2.00s   # limit_rate 128k: throughput pinned right at the ceiling

The download took exactly the 2 seconds that 262 144 ÷ 131 072 implies — the cap enforced with kernel-timer precision (NGINX paces write() calls; the client's TCP window does the rest).

The three refinements that make it production-grade:

  • limit_rate_after 1m; — the first megabyte at full speed, then the cap: instant-feeling previews, sustainable bulk. The streaming service's favorite directive.

  • Per-connection, not per-client: a client opening 6 connections gets 6× the cap — combine with limit_conn (or limit_req) when that matters. This is the classic composition:

nginx
  limit_conn  conns 2;      # no parallel-download multiplication
  limit_rate  256k;
  • proxy_download_rate — not an HTTP directive — proxy_download_rate/ proxy_upload_rate are the stream module's Layer-4 twins (Module 10); on the HTTP side limit_rate is the only pacer, and it governs NGINX→client bytes regardless of origin.

3. The composition math

The three limiters are orthogonal axes — pick per workload:

Workload

rate (requests)

conn

rate (bytes)

JSON API

✅ per-token bucket

(rarely)

—

File downloads

— (requests are rare)

✅ 1–2

✅ + limit_rate_after

WebSockets

— (one request!)

✅

—

Login form

✅ strict (no burst)

—

—

Stacking limit_req + limit_conn is normal and each evaluates independently — a request must pass every applicable gate. The example's /conn/ has no limit_req, which is why 20 rapid sequential requests never trip anything: connections are 1-at-a-time.

4. Status codes and the 429 question

limit_conn_status defaults to 503 — arguably wrong for "you have too many connections" (not a service outage). 429 is the modern choice for both limiters; the example sets limit_req_status 429 but leaves limit_conn at 503 deliberately, so §1's 503 output teaches you the default exists. Pick per fleet — and know that monitoring must distinguish "edge refused" (429/503 with $limit_conn_status: REJECTED in the log) from "backend down" (503 with empty $upstream_addr) — one log format, two very different pages.

5. What these can't do

Honest limits, stated once:

  • limit_rate can't distinguish clients' fairness beyond connections — no per-user byte budgets, no daily quotas. Those live in apps or metering systems (or njs, Module 15, counting bytes per identity).

  • limit_conn counts this NGINX's view. Behind an LB, or with multiple NGINX nodes, each counts its own connections — a fleet-wide cap of 2 per client needs the LB (or Module 10's stream layer, or Module 13's shared-state patterns).

  • Layer-4 floods (SYN, UDP amplification) never reach the HTTP limiters at all — that's Module 10's territory (and the kernel's), and pretending otherwise is how incidents happen.

Try it yourself

  1. The §1 experiment — three concurrent, third refused; then wait and succeed. Feel the slot lifecycle.

  2. Measure the cap with curl's own telemetry (§2's exact command) — then add limit_rate_after 64k; and watch the first quarter of the file arrive instantly.

  3. Break the cap with parallelism (§2's footnote): seq 1 4 | xargs -P4 -I{} curl -s -o /dev/null …/speed/256k.bin — total throughput ≈ 4× the cap. Now add limit_conn conns 2 to /speed/ and watch two of the four fail: composition in one edit.

  4. Fleet-wide vs local. Move the limit_conn consumer to http level (outside the server), reload, and verify a cap in /api/ counts connections in /speed/ too.

Q&A

Q1. A client makes 200 requests/minute through a single keep-alive connection. limit_req rate=10r/s and limit_conn 5 — what trips?

Answer

Neither, on this evidence: 200/min ≈ 3.3 r/s is under the 10 r/s bucket, and one connection is far under the cap of 5. The limiters measure different things — you'd trip limit_req by bursting past 10/s, or limit_conn by holding 6 simultaneous connections.

Q2. Why do streaming sites pair limit_rate_after with limit_rate?

Answer

UX + economics: the first N bytes at full speed make playback start instantly (and feel "fast"), while the sustained cap keeps per-user bandwidth — and transit bills — bounded for the long tail of the transfer.

Q3. Six parallel downloads each get the 256 KB/s cap — why, and what's the two-directive fix?

Answer

limit_rate is per connection; parallelism multiplies client throughput. Fix: limit_conn <zone> 1..2 (cap concurrency) so the per-connection cap can't be multiplied — §2's footnote, demonstrated live.

Q4. Your three-node NGINX fleet needs "max 2 concurrent downloads per user, globally." Can limit_conn do it alone?

Answer

No — each node counts only its own connections; a user spread across nodes gets 2 per node. The global cap must live at the shared layer: the LB, a stream-tier proxy (Module 10), or app-side metering.

Takeaways

  • limit_conn caps simultaneous per-key connections — the defense for long-lived and hostile hold-open patterns; slots free on completion.

  • limit_rate paces bytes per connection with timer precision (§2 measured 131 071 B/s against a 128k cap); limit_rate_after buys instant-feeling starts.

  • The three limiters are orthogonal axes — requests, connections, bytes — composed per workload, never confused for each other.

  • Both are per-NGINX views: fleet-wide or L4 enforcement needs the shared layer that Module 10/13 provide.

  • Status-code and log discipline ( $limit_conn_status, $limit_req_status) keeps "edge refused" distinct from "service down" in your monitoring.

Was this page helpful?

Mark this learning resource complete to track your learning progress.