Skip to main content
In this guide we explain how NGINX manages connections, the key tunables that affect scalability and latency, and practical settings to optimize performance for both client and upstream (backend) connections. NGINX uses a single master process that spawns multiple worker processes. Each worker runs an independent event loop that accepts and processes client requests and I/O events.
A slide titled "Connection Handling" showing a Master Process that distributes incoming request/response traffic to multiple Worker Processes (Worker Process 1, 2, 3, n), each containing its own Event Loop. Arrows show bidirectional communication between the master and each worker.
Each worker handles its own client connections independently, which allows NGINX to scale across CPU cores by distributing work across worker processes. How many workers should you run?
  • Recommended: use one worker process per CPU core.
Example configuration:
When worker_processes is auto, NGINX will attempt to create one worker per available CPU core. The approximate theoretical maximum number of simultaneous connections is: max_connections ≈ worker_processes * worker_connections This is an approximation — the real limit depends on reserved file descriptors (for the master, listening sockets), open upstream connections, OS limits, and additional modules. Always leave headroom when sizing. Configure worker_connections in the events block. A common starting value is 1024, and you can increase it as needed. Also adjust OS limits with worker_rlimit_nofile and ulimit -n. Example:
The practical maximum concurrent connections also depends on OS file descriptor limits and other module usage. If you increase worker_connections, raise the process file descriptor limit (ulimit -n) and consider setting worker_rlimit_nofile. Reserve some descriptors for the master process and listening sockets.
Key directives (quick reference)

Persistent connections (keepalive)

Persistent (keepalive) connections let clients reuse the same TCP connection for multiple requests (HTML, CSS, JS, images, fonts). This reduces TCP handshake/teardown overhead, lowers latency, and lowers CPU/network cost.
The image is a diagram titled "With Keep Alive" showing a browser on the left and a server on the right exchanging multiple web resource files (JS, HTML, JSON, XML, CSS). It illustrates a persistent connection meant to speed up processing and reduce CPU/network overhead.
Important keepalive directives (set in http context):
  • keepalive_requests — maximum number of requests a client can make over one keepalive connection (default ~100).
  • keepalive_timeout — time to keep an idle keepalive connection open after the last request.
Example:
Upstream (reverse-proxy) keepalive When NGINX proxies to backend servers, it can keep idle connections open to upstream servers to reuse them across proxied requests. Use keepalive in an upstream block to control how many idle connections NGINX maintains per worker.
When proxying with upstream keepalive, set the following in your location block to ensure proper HTTP version and connection handling:
  • proxy_http_version 1.1 enables persistent connections to upstreams.
  • proxy_set_header Connection "" prevents forwarding hop-by-hop connection headers so NGINX can manage pooling.
You can confirm the protocol used by a site with curl:
Look for the HTTP version in the response headers (e.g., HTTP/2 200).

HTTP protocol evolution (why connection handling changed)

A short history and why each evolution matters for connection handling:
An infographic titled "HTTP Versions" showing the evolution of the HTTP protocol as a series of human silhouettes progressing from an ape-like figure (HTTP 0.9) to a modern humanoid (HTTP 3.0). Each silhouette is labeled with a version: HTTP 0.9, 1.0, 1.1, 2.0, and 3.0.
  • HTTP/0.9 — minimal; no headers or status codes.
  • HTTP/1.0 — introduced headers and status codes.
  • HTTP/1.1 — introduced persistent connections (keepalive), chunked transfers, cache controls.
  • HTTP/2 — multiplexing multiple requests/responses over one connection, header compression (HPACK), reduced connection concurrency needs.
  • HTTP/3 — built on QUIC (UDP-based), reduces connection setup latency and improves behavior on lossy networks; requires TLS 1.3.
TCP vs UDP (brief)
  • TCP: connection-oriented, reliable, retransmits lost packets (used by HTTP/1.x, HTTP/2 over TLS).
  • UDP: connectionless, lower overhead, no built-in retransmission (used by QUIC/HTTP/3).
Usage trends snapshot Below is an example snapshot showing how usage of HTTP/2 and HTTP/3 changed over time (illustrative).
A slide titled "HTTP Versions Usage" with two side-by-side line charts: the left shows HTTP/2 usage (~34.6%) slowly declining, and the right shows HTTP/3 usage (~34.0%) generally rising with a noticeable late-year spike.

sendfile and zero-copy

Traditionally, serving a file involves copying data from disk (kernel) to user space and then back to kernel space for network transmission — consuming CPU and memory bandwidth. NGINX supports sendfile, which allows the kernel to send file data directly from disk to socket (zero-copy), lowering CPU overhead and improving throughput for static files. Enable zero-copy in the http context:
Note: behavior for tcp_nopush / tcp_nodelay is platform-specific and their interaction can affect latency vs throughput. Validate on your workload.

TCP packetization: TCP_CORK (tcp_nopush) vs TCP_NODELAY (tcp_nodelay)

NGINX exposes settings to control how data is packetized and when it’s pushed to the network:
  • tcp_nopush on; (Linux uses TCP_CORK) — delays packet transmission until a larger chunk is available, producing fewer, fuller packets (better throughput for large static responses).
  • tcp_nodelay on; — disables Nagle’s algorithm (TCP_NODELAY) sending small packets immediately to reduce latency.
Choosing between them is a trade-off:
  • Prefer tcp_nopush for serving large static files efficiently.
  • Prefer tcp_nodelay for low-latency, interactive responses.
  • You may combine both in NGINX; test combinations as results vary by OS and workload.
Changing connection and file-descriptor limits can destabilize a server if not tested. Always validate changes in staging, monitor ulimit -n, netstat / ss for socket states, and keep some descriptor/connection headroom for master/listening sockets and upstream connections.

Conclusion and next steps

Tuning connection handling in NGINX involves:
  • Setting an appropriate worker_processes (one per core recommended).
  • Sizing worker_connections and raising OS file descriptor limits (worker_rlimit_nofile, ulimit -n) accordingly.
  • Enabling and tuning keepalive for clients and upstream servers.
  • Using sendfile and appropriate TCP options (tcp_nopush, tcp_nodelay) for efficient static delivery.
  • Understanding protocol differences (HTTP/1.1 vs HTTP/2 vs HTTP/3) to choose the right approach for your traffic profile.
Recommended follow-ups: Links and references

Watch Video