← All posts
GuidesJul 2, 2026· 6 min read

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.