The two lines that decide how much concurrency an Nginx instance can physically accept are worker_processes and worker_connections. Everything else in an Nginx config — caching, compression, buffers — only matters once a request has been accepted. If these two are wrong, the box refuses connections while sitting at 15% CPU, and you spend an afternoon blaming the network. This is a sizing exercise, not a copy-paste exercise.
The arithmetic that actually binds you
Nginx documents the theoretical ceiling as worker_processes * worker_connections. That number is almost never what you get. Two corrections matter, and both are in the official docs but routinely ignored:
- Reverse proxy connections consume two slots. A proxied request holds one connection to the client and one to the upstream. A
worker_connectionsvalue of 1024 therefore yields roughly 512 proxied requests per worker, not 1024. - Every open file descriptor counts. Connections, log files, upstream sockets, and cached files all draw on the same
RLIMIT_NOFILElimit. If Nginx hits the limit, you getworker_connections are not enoughortoo many open filesin the error log — not a clean 503.
So the real ceiling is min(worker_processes * worker_connections, nofile_limit * worker_processes), with the connection half divided by two if you proxy. On a 2 vCPU VPS with the default nofile of 1024, the file descriptor limit binds long before the connection maths does. Our VPS comparison page lists how each plan approaches vCPU allocation, which is the other half of this decision.
Setting worker_processes correctly
Nginx rounds auto up to the number of CPUs it can see, not the number it can use. On a container-style VPS that reports eight host cores but enforces a two-core CPU quota, auto spawns eight workers that fight each other for 200% of CPU time. Accept events land in whichever worker is scheduled next, so latency jitter climbs even as throughput stays flat.
# Confirm what the kernel exposes versus what you actually get
nproc
cat /sys/fs/cgroup/cpu.max # cgroup v2: "200000 100000" means 2 CPUs
lscpu | grep -E "^CPU\(s\)|Model name"
# Verify Nginx agrees with you
top -H -p $(pgrep -d, nginx | head -1)
The rule: set worker_processes to the cgroup CPU quota, capped at 2× physical cores. Beyond that you gain connection slots but not throughput, and each worker carries a fixed memory cost. On a 1 GB VPS, Nginx workers are cheap — a few megabytes each — so the failure mode is not memory, it is scheduler thrash.
Raising worker_connections without starving the box
Raise worker_connections to 4096–8192, then raise the file descriptor limit in three places, because systemd applies its own ceiling on top of the unit’s limits.
| Layer | File / directive | Value |
|---|---|---|
| Kernel | fs.file-max | 2097152 |
| systemd (per-unit ceiling) | DefaultLimitNOFILE in /etc/systemd/system.conf | 65535:65535 |
| Nginx unit override | LimitNOFILE in a nginx.service.d/*.conf drop-in | 65535 |
| Nginx worker | worker_rlimit_nofile | 65535 |
Skip any layer and Nginx silently keeps the older, lower value. After a reload, confirm that cat /proc/$(pgrep -o nginx)/limits | grep 'open files' shows your target and not 1024.
Keepalive changes the concurrency you need
With keepalive_timeout 75 (the default), an idle browser holds a connection for over a minute. Two thousand visitors with persistent connections can pin thousands of slots while serving almost no traffic. Setting 15–20 seconds typically cuts the live connection count by half on a content site, at the cost of slightly more TLS handshakes. For HTTP/2, connections are multiplexed and count against http2_max_concurrent_streams instead.
worker_processes auto; # 2 on a 2 vCPU plan
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
http {
keepalive_timeout 20;
keepalive_requests 1000;
reset_timedout_connection on;
client_header_timeout 12;
send_timeout 10;
}
Measuring whether it worked
Do not trust throughput alone. Watch three signals during a load test:
# 1. Active connections and accept queue depth
curl -s http://127.0.0.1:8080/nginx_status
ss -lnt | grep ':443' # Recv-Q above 0 means the backlog is overflowing
# 2. Per-worker file descriptor usage
ls /proc/$(pgrep -o nginx)/fd | wc -l
# 3. Errors actually caused by the limits
grep -E 'worker_connections|too many open files|accept4\(\) failed' /var/log/nginx/error.log
If connection count plateaus and Recv-Q starts growing, the kernel accept backlog is full and clients are being dropped before Nginx sees them — raise net.core.somaxconn and the listen backlog value too. If errors show up in the log, you have genuinely hit a limit and raised it — likely a file descriptor ceiling, since each connection also consumes one on the kernel side. If nothing at all changes but latency improves, the win came from fewer connections, not more.
Where the tuning stops paying
Concurrency limits are a gate, not an engine. Once the gate is wide enough, the bottleneck moves to PHP-FPM workers, the database, or disk I/O — and no amount of worker_connections helps. When you see requests queueing while Nginx sits idle, that is usually the signal to check per-plan CPU and memory ceilings rather than add more config. See the full specs and pricing to confirm what your instance can sustain before tuning further.

Leave a Reply
You must be logged in to post a comment.