NGINX worker_connections and worker_processes: sizing for real traffic
NGINX defaults leave most CPU cores idle and exhaust quickly under load. The real limit is often the OS file descriptor ceiling, which silently overrides the directive.
Sizing these parameters means understanding the capacity chain: kernel queue, connection slot, file descriptor limit, event loop. This guide provides concrete rules for static and proxy workloads and the signals that reveal when headroom has disappeared.
How the worker model consumes capacity
NGINX uses an event-driven, non-blocking, single-threaded-per-worker architecture. The master process binds to ports and spawns workers. Each worker runs an independent event loop that accepts connections and moves them through a state machine. The directive worker_connections sets the maximum number of simultaneous connections each worker can track. The theoretical system-wide ceiling is worker_processes multiplied by worker_connections.
Every connection consumes a file descriptor. In reverse proxy mode, each request uses at least two connections: one client-facing and one upstream. A configuration that looks sufficient for static files may offer only half the expected capacity when proxying.
Before a worker ever sees a connection, the kernel holds it in the listen backlog. If that queue overflows, connections drop silently with zero evidence in NGINX logs. The actual capacity chain is therefore: kernel queue, then connection slot, then file descriptor limit, then event loop processing.
flowchart TD
A[Client SYN] --> B[Kernel accept queue]
B --> C[Worker accept]
C --> D[Connection slot]
D --> E{Proxy mode?}
E -->|Yes| F[2 FDs per request]
E -->|No| G[1 FD per request]
F --> H[FD limit]
G --> H
H --> I[Event loop]Sizing worker_processes
worker_processes auto spawns one worker per detected CPU core. This is the right starting point for most bare-metal and VM deployments. Because each worker is single-threaded, it can saturate a core without context-switching overhead.
In containerized environments, verify what auto actually detects. NGINX may see the host’s CPU count instead of the container’s cgroup quota, especially on older kernels. If a pod is limited to two cores but the node has sixty-four, NGINX spawns sixty-four workers. Check the running count and compare it to your quota:
# Count running workers
pgrep -c -P $(cat /var/run/nginx.pid)
If the count exceeds your allocated cores, set worker_processes to an explicit integer matching the quota. Running significantly more workers than cores wastes memory and increases scheduler overhead without improving event-loop throughput.
Sizing worker_connections
The default worker_connections is 512. Verify your current value:
nginx -T 2>/dev/null | grep -m1 'worker_connections'
For static file serving, one client connection uses one slot and one file descriptor. Max concurrent clients is approximately worker_processes times worker_connections.
For reverse proxy deployments, effective capacity is at most half that number. Each proxied request ties up two slots: one from the client to NGINX and one from NGINX to the upstream. If worker_processes is 4 and worker_connections is 1024, the theoretical maximum is 4096 connections, but the maximum simultaneous proxied requests is roughly 2048 minus idle keepalive connections on both sides.
Keepalive connections also hold slots and file descriptors while idle. A high Waiting count in stub_status is efficient but reduces headroom for new connections.
Use the headroom rule: maintain at least 2x peak active connections as configured capacity. For traffic with spikes, plan for 3-5x average peak. For example, if you must support 2,000 simultaneous in-flight proxy requests, you need roughly 4,000 connection slots. With 4 workers, set worker_connections to at least 1,000. With 3x headroom for bursts, configure 3,000.
If stub_status is enabled, read the current Active connections baseline before resizing:
curl -s http://localhost/nginx_status 2>/dev/null | awk '/Active connections/{print $3}'
The FD ceiling chain
worker_connections is only the theoretical ceiling. The hard limit is the file descriptor count, controlled by the lower of worker_rlimit_nofile and the OS-level hard limit. If worker_rlimit_nofile is unset, the system default applies, which is often 1024 and dangerously low for production.
Set worker_rlimit_nofile to at least twice the configured worker_connections. This accommodates both sides of proxy connections plus log files, temporary files, and event notification descriptors.
The effective limit at runtime may differ from nginx.conf because systemd LimitNOFILE and container runtime defaults can override it. Check the service unit:
systemctl show nginx.service --property=LimitNOFILE
Check the actual worker process limit:
worker_pid=$(pgrep -P "$(cat /var/run/nginx.pid)" | head -1)
cat /proc/$worker_pid/limits | grep 'open files'
If this runtime value is lower than worker_connections, the file descriptor limit is your real ceiling, not the NGINX directive. When workers exhaust file descriptors, they log accept4() failed (24: Too many open files) and stop accepting new connections. Existing connections continue normally, so active connections plateau while new clients time out. Search the error log for that message to confirm the bottleneck:
grep 'Too many open files' /var/log/nginx/error.log
Kernel listen backlog
Even if NGINX has free connection slots and file descriptors, the kernel can drop connections before workers see them. The listen backlog holds fully-established TCP connections waiting for accept(). The effective backlog is the minimum of the listen directive backlog and net.core.somaxconn.
NGINX defaults the listen backlog to 511. On older Linux systems, somaxconn defaults to 128, which caps the queue regardless of the NGINX setting. Modern kernels default somaxconn to 4096, but verify your system:
sysctl net.core.somaxconn
Monitor queue depth with ss -tlnp. For listening sockets, Recv-Q shows the current number of connections in the backlog and Send-Q shows the maximum. If Recv-Q approaches Send-Q, the backlog is filling. Track TcpExtListenOverflows via nstat -az | grep ListenOverflows. Any increasing counter means the kernel is dropping connections silently.
To raise the limit at runtime until the next reboot:
sudo sysctl -w net.core.somaxconn=4096
Persist the change in /etc/sysctl.conf or a systemd sysctl drop-in.
Common misconfigurations
| Misconfiguration | What it looks like | First check |
|---|---|---|
worker_processes left at default on multi-core host | CPU underutilization; latency climbs while cores sit idle | pgrep -c -P $(cat /var/run/nginx.pid) should match usable cores (or explicit quota) |
worker_connections at 512 in production | Dropped connections under moderate load; accepts - handled gap grows | nginx -T | grep worker_connections |
| Ignoring the proxy 2x multiplier | Connection exhaustion at half expected traffic volume | Compare stub_status active connections to (workers × connections) / 2 |
worker_rlimit_nofile unset or below worker_connections | “Too many open files” errors; active connections plateau below configured limit | cat /proc/<worker_pid>/limits | grep 'open files' vs configured worker_connections |
somaxconn lower than listen backlog | TcpExtListenOverflows increasing; client timeouts with zero NGINX error log entries | nstat -az | grep ListenOverflows and ss -tlnp Recv-Q approaching Send-Q |
Signals to watch in production
| Signal | Why it matters | Warning sign |
|---|---|---|
| Connection slot utilization | Approaching the hard ceiling means imminent connection drops | Active connections / (worker_processes × worker_connections) > 0.8 |
Dropped connections (accepts - handled) | Earliest indicator that capacity is exceeded | Gap growing over a 60-second window |
| File descriptor usage per worker | FD limit is the true ceiling; NGINX stops accepting when it hits | FD count > 80% of Max open files in /proc/<pid>/limits |
| Listen queue overflows | Kernel drops connections before NGINX sees them | TcpExtListenOverflows counter increasing |
| Connection state breakdown | Reveals whether pressure is from traffic, keepalive, or slow backends | Writing or Reading sustained > 50% of active connections |
| Worker process count | Validates that worker_processes matches runtime | Count deviates from configured value or CPU quota |
How Netdata helps
- Tracks
stub_statusmetrics including active connections, requests per second, and the Reading/Writing/Waiting breakdown in real time. - Alerts on connection slot utilization and the
accepts - handledgap. - Monitors per-process file descriptor usage independently of connection counts, catching cases where the FD limit is the real bottleneck.
- Collects kernel-level TCP metrics including
TcpExtListenOverflowsand listen queue depth, exposing silent drops that never appear in NGINX logs. - Correlates connection pressure with upstream response time percentiles to distinguish capacity exhaustion from backend slowness causing connection pile-up.







