nginx worker_processes and worker_connections: how to tune them, with numbers
What worker_processes auto really does, how to size worker_connections from ulimit -n, the worker_rlimit_nofile trap, and how to tell you set them wrong.
By The Opservo team
Your nginx server is running, traffic is flowing, and everything looks fine — until it doesn't. Under load, requests start queuing, latency spikes, and you're left staring at a config file that shipped with defaults tuned for nobody in particular. The two settings responsible for most of this: worker_processes and worker_connections. Getting them right is one of the highest-leverage tuning moves you can make, and it takes less than 15 minutes.
Understanding What These Settings Actually Control
Nginx uses a master process plus one or more worker processes. The master handles signals and config reloads; the workers do the actual work — accepting connections, reading requests, writing responses. worker_processes controls how many workers you spawn. worker_connections controls how many simultaneous connections each worker can hold open at once. Your theoretical maximum concurrent connections is worker_processes × worker_connections, but real-world capacity depends on your OS file descriptor limits and what those connections are actually doing.
Setting worker_processes Correctly
The most common advice is to match worker_processes to the number of CPU cores on your machine. Nginx is event-driven and non-blocking, so one worker per core is generally the sweet spot — adding more workers than cores causes context-switching overhead without meaningful throughput gains.
Find your core count with:
- nproc
- grep -c ^processor /proc/cpuinfo
- lscpu | grep '^CPU(s):'
Then set it explicitly in /etc/nginx/nginx.conf:
worker_processes 4; # match your core count
Better yet, use the auto keyword — nginx will detect core count at startup:
worker_processes auto;
Use auto unless you have a specific reason to pin workers to fewer cores, such as leaving headroom for other CPU-intensive processes (a database, a Node app) running on the same host. If nginx shares a box with Postgres, consider setting workers to half your core count and benchmarking from there.
Tuning worker_connections and File Descriptors
worker_connections defaults to 512 or 1024 depending on your nginx build. For most production servers that's too low. A single page load can open multiple connections, and if you're proxying to upstream services, nginx holds connections to both the client and the backend simultaneously — effectively halving your usable connection budget.
A reasonable starting point for a dedicated nginx server is 1024–4096 per worker. Set it inside the events block:
events { worker_connections 2048; use epoll; # Linux default, but explicit is better multi_accept on; }
multi_accept on tells each worker to accept as many new connections as possible in one go rather than one at a time. On high-traffic servers this measurably reduces accept latency.
Here's the catch: nginx can't open more connections than the OS allows file descriptors per process. Check your current limit with:
- ulimit -n # for your current shell
- cat /proc/$(pgrep -o nginx)/limits | grep 'open files'
If the limit is lower than your worker_connections, nginx will log 'too many open files' errors and drop connections silently. Raise it by setting worker_rlimit_nofile in the main nginx.conf context (outside any block):
worker_rlimit_nofile 65535;
Also raise the system-wide limit in /etc/security/limits.conf or via a systemd override if nginx runs as a service:
# /etc/systemd/system/nginx.service.d/override.conf [Service] LimitNOFILE=65535
Run systemctl daemon-reload && systemctl restart nginx after any systemd change.
A Practical Baseline Config
Here's a full events block plus the top-level directive that works well for a 4-core dedicated nginx server handling mixed web and proxy traffic:
worker_processes auto; worker_rlimit_nofile 65535; events { worker_connections 4096; use epoll; multi_accept on; }
After saving, always test before reloading: nginx -t. If that passes, reload with systemctl reload nginx (zero downtime) rather than restart.
How to Know If Your Tuning Is Actually Working
Don't tune blind. A few things to watch after making changes:
- nginx's built-in stub_status module — enable it on a private endpoint to see active connections, accepts, and handled counts in real time
- dmesg and /var/log/nginx/error.log — search for 'worker_connections' or 'too many open files' to confirm you've cleared bottlenecks
- ss -s or netstat -an | grep ESTABLISHED | wc -l — see actual concurrent connections at the OS level
- top or htop — watch per-core CPU usage; if workers are maxing out single cores while others idle, something upstream (slow upstreams, keepalive misconfiguration) is the real problem
Load testing with a tool like wrk or hey before and after changes gives you a concrete baseline: wrk -t4 -c200 -d30s https://yoursite.com. Focus on requests/sec and latency p99, not just averages.
If you're running multiple services on the same host and find yourself piecing together signals from error logs, stub_status, and system metrics every time something looks off, this is exactly the kind of ongoing tuning work that Opservo is built to surface automatically — flagging connection saturation and misconfigured limits before they turn into incidents. But even without tooling, the config above will put most small-to-medium nginx deployments in a significantly better position than their out-of-the-box defaults.