I scale Nginx workers specifically to handle thousands of concurrent requests with low Latency to operate. The key lies in a balanced combination of `worker_processes`, `worker_connections`, file descriptors, and Events.
Key points
- Capacity = worker_processes × worker_connections; in the case of a reverse proxy, this is often determined by the client and upstream connections doubled.
- File descriptors (worker_rlimit_nofile, ulimit) to match the expected connection load lift.
- Events-Block with epoll, multi_accept, and kernel backlogs under high load trim.
- Monitoring via stub_status and load tests for iterative Customization.
- Scaling Combine vertically and horizontally, configuration decouple.
NGINX Architecture: Master, Worker, and Events
NGINX uses a master process that launches multiple worker processes and efficiently manages them with Events handles. Instead of processing one thread per request, each worker handles numerous connections non-blocking via an event-driven model with low Overhead. I set the `worker_processes` directive to `auto` so that NGINX can utilize the CPU cores and assign a separate worker to each process. This allows me to distribute incoming connections more effectively and keep latency low during peak loads low. For a more in-depth look at process planning, please see Optimize Worker Processes, because proper parallelization determines the achievable connection capacity. It is crucial that the `worker_connections` per worker are appropriately sized so that multiplying them by the number of processes yields the expected Peak load covers.
Capacity formula: worker_processes × worker_connections
I calculate the rough capacity as `worker_processes` × `worker_connections`, although proxied requests often use two connections per user session, which effectively halves the number. can. Many default installations start with 512 connections per worker, which is often insufficient for production workloads is. Practical default values typically range from 1024 to 4096 and depend on the traffic profile and hardware. I plan with headroom—that is, at least a factor of two above the measured peak load—to safely handle bursts to cushion. It remains important to validate these findings through testing and live metrics so that the numbers don't become a mere theoretical exercise become.
| Scenario | worker_processes | worker_connections | Theoretical maximum. | Effective (Proxy) | FD per worker |
|---|---|---|---|---|---|
| Small website | 2 | 1024 | 2048 | ~1024 | ≥1024 |
| API Medium Load | 4 | 2048 | 8192 | ~4096 | ≥2048 |
| Store Peak Hours | 8 | 4096 | 32768 | ~16384 | ≥4096 |
HTTP/1.1, HTTP/2, and TLS: Impact on Workers and Latency
Protocols determine the connection profile. With HTTP/1.1, I often see many concurrent TCP connections per client, whereas HTTP/2 reduces these to just a few streams that are, however, utilized more heavily. bundles. This saves on file descriptors, but shifts the load to buffers and prioritization. With TLS, I make sure to reuse sessions so that costly handshakes don't occur with every request slow down. A shared session cache and appropriate timeouts reduce CPU spikes. I also make sure not to set `keepalive_requests` too low, so that long-lived connections can deliver their benefits play out. For HTTP/2, I calculate higher concurrency per connection and ensure that the send/receive buffers are large enough, without using up memory waste. With mixed traffic, I plan conservatively and verify the effects for each protocol variant in the Test.
Set file descriptors and ulimit correctly
Every connection requires at least one file descriptor; reverse proxies often require two, which is why low `ulimit` values can cause serious Boundaries Set. I increase `worker_rlimit_nofile` so that `worker_processes` × `worker_connections` is feasible and there is room for logs, sockets, and caches. System-wide, I adjust `limits.conf` and `fs.file-max` so that the operating system allows the planned number of open files and does not run out of space prematurely brakes. I use `ulimit -n` and Systemd parameters (LimitNOFILE) to check whether the configuration persists and is compatible with NGINX. If you ignore this setting, you'll suddenly experience rejected connections and rising Latencies.
Fine-Tuning the Events Block: epoll, multi_accept, Backlogs
On Linux, I use epoll because this mechanism efficiently handles large numbers of connections using asynchronous Events handles. With `multi_accept` set to `on`, a worker accepts multiple new connections per event, which smooths out load spikes and reduces acceptance delays lowers. I adjust kernel parameters such as net.core.somaxconn and net.ipv4.tcp_max_syn_backlog as needed to prevent accept queues from overflowing during traffic spikes. TIME_WAIT optimizations such as tcp_tw_reuse reduce port bottlenecks and maintain the throughput curve high. For a more in-depth look at concurrency and queues, it's worth checking out Thread pool optimization, even though NGINX operates primarily on an event-driven basis and is therefore very resource-efficient scaled.
Properly Allocating Listen Sockets: reuseport, backlog, and accept_mutex
When there are a large number of simultaneous connections, I actively scale the reception path. With reuseport Each worker is assigned its own listening socket; this eliminates contention during the `accept` operation and distributes the load evenly across all cores. I explicitly set the listen backlog to handle short bursts of traffic. The `accept_mutex` is no longer needed in this setup. Without `reuseport`, however, `accept_mutex` help, to mitigate herd effects during acceptance. Important: Backlog sizes in NGINX and the kernel (somaxconn) should fit together, otherwise the effect will be lost.
events {
use epoll;
worker_connections 4096;
# accept_mutex on; # is usually not necessary with reuseport
}
server {
listen 443 ssl http2 reuseport backlog=65535;
# ...
}
In addition, I pin workers to CPU cores as needed (worker_cpu_affinity) to keep cache lines and IRQ load stable. In heavily NUMA-oriented environments, this reduces unnecessary Cross-traffic in memory.
Reverse Proxy, Upstreams, and Keep-Alive
As a reverse proxy, NGINX often maintains two connections per request: one to the client and one to the backend, which makes capacity planning realistic double counts. I enable Keep-Alive when appropriate so that upstream connections remain reusable and the overhead per request sinks. This way, I reduce the load on PHP-FPM, the application server, or microservices and free up slots for new user sessions. The balance between timeouts, idle time, and reuse determines how efficiently connections are recycled become. If you'd like to read up on the basics, you'll find them in Persistent Connections Practical tips on utilization and improving network—Use.
Upstream Pools, Timeouts, and Retry Attempts
To prevent workers from waiting for slow backends, I use short timeouts and carefully calibrated retries. I keep upstream keepalive pools large enough to keep connections warm, but not so large that inactive file descriptors take up memory and slots bind. I limit retries to just a few attempts and only switch over in the event of clear transport errors—this way, I prevent "thundering herd" effects during brief backend outages.
upstream app_backend {
server 10.0.0.11:8080 max_fails=3 fail_timeout=10s;
server 10.0.0.12:8080 max_fails=3 fail_timeout=10s;
keepalive 64; # reusable upstream connections
}
server {
location / {
proxy_pass http://app_backend;
proxy_connect_timeout 2s;
proxy_read_timeout 15s;
proxy_send_timeout 15s;
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;
}
}
At the same time, I adjust keep-alive parameters (timeouts, requests per connection) to quickly free up resources from clients that are rarely active to release.
Plan for Scalability Wisely: Combine Vertical and Horizontal Scaling
For high traffic volumes, I combine vertical and horizontal scaling in Consider. I scale vertically by adding more CPU cores, RAM, fast SSDs, and an optimized network configuration so that each worker runs smoothly works. I scale horizontally using stateless NGINX nodes, centrally managed configuration, and distributed logging, so that the total capacity scales linearly grows. Local caches and well-defined policies via Maps or the API make it easy to roll out changes quickly. This separation reduces side effects and helps accommodate new traffic patterns without requiring modifications to each node serve.
Hosting Perspective: Latency, Error Rates, and User Experience
Too few `worker_connections` result in rejected connections, timeouts, and poor User experience. Dynamic applications such as CMSs or online stores notice this immediately, because a page view generates multiple backend requests and slots are filled more quickly short . That's why I start with moderate values like 1024 or 2048 per worker and increase them gradually based on actual measurements. At the same time, I ensure that the upstream services remain efficient and provide enough file descriptors so that no artificial Limits perform. Benchmarks show that carefully tuned platforms deliver real advantages here and reliably handle peak traffic intercept.
Memory, Buffering, and I/O Paths
Each connection uses memory for metadata and buffers. I set the values for `proxy_buffers`, `client_body_buffer_size`, and `large_client_header_buffers` so that typical requests fit without using too much RAM across the board for outliers. bind. For static content, `sendfile` and `tcp_nopush` speed up delivery, while `tcp_nodelay` is used for latency-critical small responses important remains. If assets are stored on slower storage, `aio threads` plus `thread_pool` help mitigate blocking effects. I use `open_file_cache` to reduce file accesses and `stat()` calls, but keep in mind the additional file descriptor (FD) requirements. I write logs in a buffered manner (access_log … buffer=… flush=…) so that I/O spikes do not affect response times influence.
Balancing Security and TLS Performance
TLS handshakes are CPU-intensive. I combine session reuse with moderate key parameters and enable stackable optimizations such as session caches and tickets, provided they are operationally fit. The sweet spot between security and performance keeps latencies stable without compromising cipher quality. Under higher load, I monitor the 95th and 99th percentiles separately, since TLS spikes would otherwise be masked by average values hide. HTTP/2 reduces the number of connections, but requires careful attention to flow control and header compression to keep CPU and memory usage under control retain.
Resilience Under Stress: Limits and Gentle Release
To maintain latency, targeted Shaping Essential during peak load. I use `limit_conn` to limit the number of concurrent connections per key (e.g., IP or session), and `limit_req` throttles burst traffic and protects backends from synchronous Storming. I isolate critical endpoints using stricter rules than static assets. If traffic spikes suddenly, I return well-defined 429/503 responses with a "Retry-After" header, rather than handling all requests uniformly starve to death I pause lingering connections (lingering_close) to release resources in a controlled manner and to prevent Slowloris patterns from refute. This active shedding keeps the p95/p99 latency within the acceptable range, even when total demand temporarily exceeds the rated capacity lies.
Container and System Integration: Removing Limits Where They Arise
Containers often have stricter limits. I check cgroup limits (CPU, RAM), set `ulimit -n` appropriately within the container, and hardcode `LimitNOFILE` in the service definition. sysctl parameters such as `somaxconn` and `tcp_max_syn_backlog` must be set on the Host take effect; namespaces do not always isolate these settings transparently. On orchestrated platforms, I plan capacity per pod/node, pin workers to assigned cores, and ensure stable network paths (e.g., no unnecessary NAT hops) so that the latency curve quiet remains. I use `worker_shutdown_timeout` during rolling updates to ensure that existing connections are closed cleanly run out.
Monitoring and Iterative Optimization
Without visibility, tuning steps remain Risk. I enable `stub_status` or alternatives to continuously monitor active connections, acceptance rates, and rejections. In load tests, I simulate realistic access patterns and identify bottlenecks in accept queues, upstream latencies, or CPU-Saturation. After that, I carefully adjust `worker_connections`, processes, file limits, and TCP parameters, and verify the effect again. This cycle keeps the platform running reliably and prevents surprises at inopportune Times.
Sample Configuration and Calculation Method
Assuming I expect 2,000 concurrent in-flight requests during peak times and am using a reverse proxy, I'll roughly calculate 4,000 connection slots plus Buffer. If NGINX is running on four CPU cores, I typically start with `worker_processes auto` and `worker_connections` set to 1,000 to 2,000 per worker. I set the file descriptor limit high enough per worker to ensure that connections, logs, and internal sockets have sufficient Place I set the events block to epoll, enable multi_accept, and increase the kernel backlogs to match my peak traffic. A minimalist snippet might look like this, which I then fine-tune using benchmarks vote:
worker_processes auto;
worker_rlimit_nofile 65535;
events {
use epoll;
worker_connections 2048;
multi_accept on;
}
http {
keepalive_timeout 65;
sendfile on;
# additional proxy/cache options ...
}
Additionally, I'm implementing list and upstream optimizations to fine-tune the acceptance and backend paths under load:
events {
use epoll;
worker_connections 4096;
# worker_cpu_affinity auto; # Assign cores statically if necessary
}
http {
# Example TLS/session optimizations
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1h;
upstream app_backend {
server 10.0.0.11:8080;
server 10.0.0.12:8080;
keepalive 64;
}
server {
listen 443 ssl http2 reuseport backlog=65535;
location / {
proxy_pass http://app_backend;
proxy_connect_timeout 2s;
proxy_read_timeout 15s;
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;
}
}
}
In a nutshell: Concrete guidelines
I adjust `worker_processes` to match the number of CPU cores, and I typically set `worker_connections` to between 1024 and 4096. For the reverse proxy, I plan for two connections per request and allow for at least double the headroom relative to the measured peak—Load. I set `worker_rlimit_nofile` and system-wide limits high enough so that the values in `nginx.conf` remain usable in practice. I configure the `events` block to use `epoll` and `multi_accept`, while kernel backlogs handle short traffic spikes cushion. Through monitoring and incremental adjustments, I turn this into a reliable traffic engine that smoothly handles growing visitor numbers carries.


