...

Optimally Configuring NGINX Worker Processes for Maximum Performance

I configure NGINX Worker so that `worker_processes`, `worker_connections`, and `worker_rlimit_nofile` are set exactly right and epoll works in the event loop. This allows me to use CPU cores Be efficient, scale concurrent connections in a predictable manner, and keep latency low during peak loads.

Key points

The following key points provide you with immediate guidance for a robust NGINX worker configuration.

  • worker_processes Link it to the number of logical cores, ideally using „auto“.
  • worker_connections Set it so that actual peaks are easily covered.
  • rlimit_nofile and increase the OS limits to match the connection volume.
  • epoll and enable `multi_accept` to make efficient use of the event loop.
  • Load tests Proceed and fine-tune in small steps.

NGINX Architecture: Understanding Master and Worker Nodes

I separate the tasks from Master and workers: The master loads configurations, opens sockets, and starts processes, while the workers process requests in the event loop. Each worker runs independently, responds to events, and can manage thousands of connections without causing deadlocks. This model excels when I allocate CPU cores appropriately and optimize the event loop using epoll. I keep in mind that every additional proxy hop consumes connection resources, which is reflected in the limits. Those who understand the roles can make informed decisions about Resources and prevents bottlenecks early on.

Correctly Linking the Three Key Directives

I consider worker_processes, worker_connections, and worker_rlimit_nofile should never be set in isolation, but rather as a unified set. The total number of possible connections is calculated by multiplying the number of workers by the number of connections per worker; from this, I derive the limits for file descriptors. If these settings aren’t aligned, I run into „too many open files“ errors or experience hard timeouts. For high loads, I need a well-coordinated setup: sufficient processes, generous connection limits, appropriately increased `rlimit_nofile`, and suitable OS parameters. This prevents a limit that’s too small from Limit the entire capacity is reduced.

worker_processes: Select a specific number

I set worker_processes It is usually set to „auto“ so that NGINX can detect the number of logical CPU cores and utilize each core. One worker per core avoids unnecessary context switches and distributes the load evenly, which keeps response times predictable. On machines with a very large number of cores, I deliberately test lower worker counts as well to compare cache hits and core utilization. If the metrics show that cores are overloaded or TLB misses are increasing, I adjust the number of workers incrementally. Measure first, then adjust—that’s how I ensure reliable Results.

worker_connections: Increase connections as needed

I choose the worker_connections Depending on target traffic and protocol mix, this often starts at 2048 or 4096. For high-traffic APIs, I consider 8192, provided that OS limits and RAM allow for it. I verify every increase with load tests, because open connections tie up memory and affect upstream behavior. If SSL handshakes or large uploads dominate, I focus more on CPU and I/O profiles rather than just raw connection counts. This ensures that the number defined per worker Capacity remains usable in practice.

Synchronize `worker_rlimit_nofile` and OS limits

I'll make sure that rlimit_nofile covers at least the theoretical total capacity and is often configured with a margin. For reverse proxy scenarios, I factor in a second descriptor per client connection to the upstream server. Accordingly, I like to set `rlimit_nofile` to twice the expected number of concurrent connections. I raise the kernel and user limits (ulimit -n, fs.file-max) so that NGINX can actually utilize these values. If messages about open files appear in the error log, I increase the limits promptly and monitor the Latency again under load.

Events Block: Using epoll and multi_accept Effectively

I'm enabling it in the Events block epoll and set `multi_accept` to „on“ so that workers accept pending connections in a single pass. Epoll reduces overhead when there are many concurrent sockets and aligns well with NGINX’s non-blocking design. These settings pay off during traffic spikes because they speed up the connection acceptance phase and allow me to move more quickly to the actual processing. For Linux, this is my default configuration, which I only change in rare, special cases. If you want to dive deeper, compare the event-loop model with Thread Pool vs. Event Loop and draws the following conclusion conclusions for one's own environment.

CPU Affinity: Bind Workers to Cores

I set worker_cpu_affinity I use this approach specifically when workloads are constant and CPU-bound. I distribute the binding scheme using bitmasks to avoid context switches and promote cache locality. With four cores, I assign the masks so that each worker gets its own core. I then check cache miss rates, median latencies, and 99th percentiles to clearly see the effect. You can find a concise explanation of affinity and NUMA at CPU Affinity in Practice, which is useful when fine-tuning Worker-Layouts helps.

Capacity Planning: Headroom and Load Testing

When making connections, I plan a Buffer a value that is significantly higher than the observed peaks, so that short-term spikes don't immediately hit the limits. If I double the peak load as a starting point, I have a solid margin of safety in many scenarios. When traffic fluctuates heavily, I increase the buffer further until the 99th percentiles run smoothly. Then I check for bottlenecks using tools like wrk or k6, monitor error rates, and review open connections in the status section. Only when the metrics are consistent do I selectively increase or decrease individual Values.

Configuration and Sample Calculations

I calculate the connection capacity by multiplying the number of workers by the number of connections per worker, and then set the limits slightly higher based on that calculation. With four CPU cores, "auto," and 4,096 connections per worker, my calculation yields 16,384 concurrent connections. In proxy scenarios, I tend to set `rlimit_nofile` to 32,768 or higher to ensure that upstream sockets are included. For small machines with two cores, 2,048 connections per worker are often sufficient, provided that the proportion of uploads and TLS traffic remains moderate. The following table helps in classifying the starting values:

CPU cores worker_processes worker_connections (Start) Min. rlimit_nofile (Guideline) Note
2 auto (≈2) 2048 ≥ 4096 Reserve Schedule for TLS/Proxy
4 car (≈4) 4096 ≥ 16,384 With proxies, the factor is often 2 for FDs
8 car (≈8) 4096-8192 ≥ 32,768 Load test decides on an increase
16+ car, or possibly fewer 8192+ ≥ 65535 Test with sensitivity and discretion

NGINX Workers and Upstreams: Weighing Scenarios Correctly

I distinguish between static delivery, reverse proxy operation, and API gateway load because they are the Worker-Configuration varies depending on the scenario. Static content consumes fewer resources, while TLS, compression, and upstream connections place a heavier load on the CPU and file descriptors. The larger the SSL keys and the more handshakes there are, the more the „one worker per core“ setting benefits the system. Large uploads shift the focus to I/O, which leads me to pay closer attention to `rlimit_nofile` and network buffers. When there are noticeable delays in acceptance or backend responses, this overview helps me to Queues and latency, to avoid bottlenecks targeted to solve.

Practical Workflow: Step-by-Step Guide to a Faster Server

I'll start by taking stock of all relevant Values In the nginx.conf file, check the CPU cores, ulimit, and kernel parameters. Then I set `worker_processes` to `auto`, set `worker_connections` to 4096, for example, and increase `rlimit_nofile` generously. In the events block, I enable epoll and multi_accept, then reload the server to check the logs. This is followed by load tests under reproducible conditions, during which I monitor response times, error rates, and open connections. During the fine-tuning phase, I always change only one variable at a time, document each step, and check the effects in the Metrics.

Hosting Environment: Resources, Kernel, Network

I make sure to get enough CPU-Cores, sufficient RAM, fast SSDs or NVMe drives, and a recent Linux kernel. This is the only way to ensure that epoll, modern TCP stacks, and useful offload features work reliably. I adjust network parameters such as `somaxconn` and `tcp_max_syn_backlog` to match the target number of connections in order to keep acceptance queues short. A provider with robust I/O performance and freely accessible system configuration clearly pays off in this regard. Comparisons show that services with consistent Resources Significantly expand NGINX's capabilities.

Keepalive Strategy: Client and Upstream Connections

I deliberately use keepalive as a way to manage capacity and latency. On the client side, I set keepalive_timeout not too high, so that inactive sockets aren't unnecessarily worker_connections block. Values between 10 and 30 seconds often provide a good balance between reuse and resource consumption. With keepalive_requests I limit the number of requests per connection to cut off long-running requests and avoid memory pressure. On the upstream side (reverse proxy), I maintain persistent connections with keepalive in the upstream block, so handshakes and TCP setup are not required. I scale the number per backend conservatively based on the backend capacity (max_conns), otherwise I handle the queues myself on the upstream side. Important: Each keepalive socket counts as an open connection and requires file descriptors (FDs); I take that into account in rlimit_nofile and my headroom planning.

List Optimization: Reuseport, Backlog, and Accept Strategy

I distribute the load evenly by SO_REUSEPORT activate (listen … reuseport). Each worker thus has its own accept queue, which reduces „thundering herds“ and prevents hotspots. In combination with multi_accept I noticeably speed up the acceptance phase. The list-backlog I set (listen … backlog=) and the kernel counterparts (somaxconn, tcp_max_syn_backlog) to generous values so that traffic spikes don't get lost at the socket input. The option deferred Postpones acceptance until data is available—this can help with many short-lived requests; otherwise, I compare the results in tests. Whether I accept_mutex I determine what I need based on the benchmark: With reuseport, it’s usually unnecessary; without reuseport, it can improve fairness but requires coordination. I make this decision based on data, never on a hunch.

Set Timeouts and Queues to Stable Values

I set Timeouts so that slow clients don't clog up the workers: client_header_timeout and client_body_timeout I keep it concise enough to avoid glitches, but generous enough for real users. send_timeout Prevents responses to the client from being blocked. In the proxy context, I define proxy_connect_timeout, proxy_read_timeout and proxy_send_timeout stringent, so that hanging backends don't cripple the frontend. For backends with limited parallelism, I use queue in the upstream block with a timeout to cushion spikes and signal 503 in a controlled manner, rather than tying all workers to waiting upstream sockets. Additionally, I stabilize the system by limit_req (Burst/Delay) and limit_conn sensitive paths, so that individual clients or bots do not consume a disproportionate amount of resources.

Buffering, sendfile, and AIO: Choosing I/O Methods Carefully

I set sendfile for static files and combine it with tcp_nopush/tcp_nodelay depending on the workload, to efficiently bundle packets or reduce interactive latency. For large files, I use directio above a certain threshold, to prevent cache pollution and ensure that the page cache is not overwritten. In proxy mode, I decide whether proxy buffering helps (fast handoff to the client, decoupled upstream reading) or whether I should instead, when dealing with streaming workloads, proxy_request_buffering reduce to trigger uploads early. The sizes of proxy_buffers, proxy_buffer_size and large_client_header_buffers I manage this deliberately so that memory usage per connection doesn't skyrocket. To minimize the load on the CPU during file access, I'm considering aio (native or threads), but test thoroughly, because event loop and I/O characteristics influence each other.

HTTP/2, HTTP/3, and TLS: Impact on Worker Capacity

I take into account that HTTP/2 and HTTP/3 Changing connection dynamics: Many requests run as streams using a small number of TCP or QUIC connections. This reduces the number of connections but increases CPU and memory usage per connection (multiplexing, header compression, TLS/QUIC). My worker_connections Therefore, I don't automatically interpret this as „the same number of requests.“ I observe concurrent streams per connection and pass keepalive_timeout and, if applicable,. http2_max_concurrent_streams . On the TLS side, I benefit from session resumption (tickets/cache) and OCSP stapling; this allows me to avoid costly handshakes and keep latency low. The downside: Longer keepalives tie up file descriptors and RAM—so I plan rlimit_nofile and memory quotas with realistic reserves. For CPU-intensive ciphers, it’s worth testing with affinity and modern cryptographic acceleration.

Observability: Status, Logs, and Metrics

I create transparency with a streamlined Status-Endpoint (e.g., stub_status) to view active connections, reading/writing/waiting states, and accepted requests. I keep log noise to a minimum: A compact log_format Including time, status, upstream times, and bytes is sufficient for most analyses. When QPS is very high, I selectively disable the access log (based on location) or buffer logs asynchronously so that I/O doesn't slow things down. I set the error log to warn or error and switch to it only briefly for specific analyses debug. I continuously correlate latencies (median/95th/99th percentile), open connections, backend error rates, and CPU load per worker—from this, I derive adjustments to the three key directives and identify saturation effects early on.

Containers and Virtual Environments: Passing Limits Cleanly

I check the following in containers: cgroup-Set limits for CPU, RAM, and PIDs, and align them with the NGINX settings. ulimit -n must be high enough within the container, otherwise my `rlimit_nofile` adjustments will be ignored. For CPU quotas (e.g., 2 vCPUs), I set worker_processes Accordingly, to ensure that scheduling doesn’t artificially crowd out processes. When close to the network, I benefit from lower overhead latency in „host“ network modes, while overlays involve additional hops. On multi-NUMA hosts, I pay attention to affinity and memory sockets to ensure that workers do not operate across nodes. The same applies to IRQ and RPS/XPS affinity: if the paths from the NIC through the IRQ to the worker core are aligned, latency spikes decrease measurably.

Connection Lifecycle: Ephemeral Ports, TIME_WAIT, and Reserves

I plan to have enough ephemeral ports (ip_local_port_range) when NGINX acts as an active client to upstream servers. With very high connection throughput, I avoid excessive port fluctuation by using upstream keepalive, which reduces TIME_WAIT stacks. I use kernel toggles for „Reuse“ only with caution; modern stacks already optimize many aspects internally. It is more stable to control the duration of connections using sensible keepalive and timeout values and reuseport to ensure fair distribution. When calculating capacity, I always take the upstream side into account in addition to clients—often the FDs there are the actual limiting factor, not the front door.

Graceful Reloads and Deployments Without Downtime

I use the master/worker model for graceful reloads: The master loads new configurations; old workers shut down while new ones seamlessly take over. With worker_shutdown_timeout I give requests time to complete properly without blocking resources. I combine zero-downtime deployments on the upstream with health checks and proxy_next_upstream-Rules to ensure that individual malfunctioning backends don't drive up overall latency. When making configuration changes, I always adjust only one setting at a time and verify the effects in logs and metrics—this helps me avoid errors caused by confusion and ensures that performance remains reproducible.

Concise summary

I'm pairing worker_processes Set the number of cores (ideally "auto"), configure `worker_connections` to match the peak load, and generously increase `rlimit_nofile` along with OS limits. In the `events` block, I use `epoll` and `multi_accept`, test everything with reproducible load tests, and then fine-tune in small increments. For proxy workloads, I factor in additional descriptors and test CPU affinity when workloads are constant. A properly configured stack with the right kernel, fast I/O, and sensible network parameters makes all the difference. That’s how I achieve NGINX reliably delivers the performance required by demanding websites and APIs.

Current articles