The nginx Worker Math That Actually Applies to a Laravel Stack
Most nginx tuning guides throw numbers at you without context. Here's the math I actually use for single-VM Laravel deployments.
Every nginx tuning article eventually tells you to set worker_processes auto and worker_connections 1024 and then moves on like that's the end of it. It isn't. Those defaults will get you through a lot, but when something goes sideways under load — and it will — you need to understand the actual math, not just the cargo-culted config.
I've tuned single-VM Laravel stacks for a decade across healthcare portals, e-commerce sites, real estate platforms, and a handful of industrial dashboards. The math is not complicated, but you have to apply it to your machine, your PHP-FPM pool config, and your expected concurrency. Here's how I actually think through it.
What These Directives Actually Control
nginx has a master process and worker processes. The master reads config and manages workers. Workers do all the actual I/O — accepting connections, proxying to PHP-FPM, serving static files, talking to upstream services.
worker_processes is how many worker processes nginx spawns. Each one is single-threaded. They don't share connections with each other.
worker_connections is the maximum number of simultaneous connections each worker can handle. Not the total — per worker. So total potential connections = worker_processes × worker_connections.
That product is the number people always forget to think about in terms of operating system limits and PHP-FPM pool size. I'll come back to both.
Start With Your CPU Count
For worker_processes, the right answer for a CPU-bound workload is one worker per logical CPU core. nginx worker processes are mostly doing I/O — they spend very little time in CPU — so for a pure proxy setup you could go higher, but on a shared VM that's also running PHP-FPM, MySQL, and Redis, you're competing for cores. I set it to the core count or use auto, which does the same thing.
worker_processes auto;
On a 4-core VM (the most common size I provision for mid-size Laravel apps), that's 4 workers.
The exception: if you're on a box with a high core count (say, 16+ cores) but your app is mostly small PHP requests with fast upstream responses, you can sometimes get better CPU cache locality with fewer workers. I haven't needed to go that route on any client stack yet — if you're on a 16-core machine and it's a single Laravel app, there are probably other problems to solve first.
The worker_connections Math
This is where most people set 1024 and stop thinking. Let's actually think.
A single browser request to a PHP page often involves more than one nginx connection: the downstream client connection, and the upstream connection to PHP-FPM. nginx counts both. So in the worst case, one user request consumes 2 worker connections. For a request that also proxies an API or fetches a file from an upstream, it could be more, but for a typical Laravel app behind nginx talking to PHP-FPM via a Unix socket, budget 2 connections per request.
So if I want each worker to handle 512 concurrent requests:
worker_connections = 512 requests × 2 connections/request = 1024
With 4 workers, that's 4096 total simultaneous connections. Fine for most apps.
But here's the actual constraint that matters more than this number: your PHP-FPM pool size. nginx can queue connections all day, but if PHP-FPM only has 20 processes and they're all busy, requests sit in the nginx upstream queue. Setting worker_connections 4096 doesn't help you if your FPM pool max is 25.
Matching nginx to PHP-FPM
This is the part nobody talks about in the nginx articles because it crosses tool boundaries.
For a Laravel app, I size the PHP-FPM pool based on available RAM minus what nginx, MySQL, and Redis eat. A typical Laravel worker process uses 30–80 MB depending on what's loaded. On a 4 GB VM:
- OS + nginx: ~200 MB
- MySQL: ~512 MB (tuned)
- Redis: ~100 MB
- Remaining for PHP-FPM: ~3.2 GB
- PHP-FPM worker memory budget: ~60 MB average
- Max FPM workers: ~53, so I set pm.max_children = 50 with headroom
With 50 FPM workers, that's my actual concurrency ceiling. nginx can accept more connections than that, and it will — they just queue. The nginx worker_connections number needs to be high enough that it's not the bottleneck before FPM is. If FPM is my ceiling at 50 concurrent requests, I need nginx worker connections to comfortably hold more than that in queue.
Here's a real /etc/nginx/nginx.conf skeleton I use as a starting point for these stacks:
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 30;
keepalive_requests 1000;
# Upstream PHP-FPM pool (Unix socket is faster than TCP on same box)
upstream php-fpm {
server unix:/run/php/php8.3-fpm.sock;
keepalive 32;
}
server {
listen 80;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass php-fpm;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
# Don't let slow PHP hold nginx connections open forever
fastcgi_read_timeout 60;
fastcgi_send_timeout 60;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
}
}
Few things worth calling out:
worker_rlimit_nofile 65535— this raises the open file descriptor limit for nginx workers. Without this, the OS default (usually 1024) can throttle you beforeworker_connectionsever does. I've been bitten by this on a busy e-commerce site where nginx was dropping connections and the error log just said "too many open files." Add it every time.use epoll— Linux only, but you're on Linux. It's the right event model for high connection counts.multi_accept on— tells each worker to accept all pending connections at once instead of one at a time. Low cost, useful under bursty traffic.keepalive 32on the upstream block — keeps persistent connections open to PHP-FPM. Reduces socket handshake overhead on high-request-rate apps.fastcgi_read_timeout 60— this one trips people up. The default is 60 seconds, which sounds fine until you have a Laravel job accidentally running in a request cycle or a slow report query. I set it explicitly so it's visible and intentional, and I tune it down to 30 for most public-facing routes.
The File Descriptor Gotcha
I want to dwell on worker_rlimit_nofile for a second because it's the most common thing I see missing from nginx configs in the wild.
Every connection nginx holds open consumes a file descriptor. So does every open log file, every upstream socket. The system default ulimit -n on a fresh Ubuntu box is 1024. If you've set worker_processes 4 and worker_connections 1024, you're theoretically handling up to 4096 connections, but each worker process is capped at 1024 file descriptors by the OS. You'll hit the OS limit before the nginx limit and get cryptic errors.
The fix:
# In the main context (outside events {})
worker_rlimit_nofile 65535;
And in /etc/security/limits.conf or the nginx systemd service override:
nginx soft nofile 65535
nginx hard nofile 65535
Verify it took with cat /proc/$(pgrep -o nginx)/limits | grep open.
When to Reach for These Knobs
If your site is serving under a few hundred concurrent users on a reasonably sized VM, worker_processes auto and worker_connections 1024 will not be your bottleneck. Slow queries, un-cached PHP, missing indexes — those are the bottlenecks. Don't let nginx tuning become a distraction from the real work.
I actually reach for this tuning in a few specific situations:
- Right before a known traffic event. A client e-commerce site running a promo, a healthcare portal expecting a burst enrollment period. I'll audit the whole stack — FPM pool, nginx config, MySQL buffer pool — as part of that prep.
- After seeing
worker_connections are not enoughin the nginx error log. That's the signal to bumpworker_connectionsand recheck your FPM pool ceiling. - When migrating to a bigger VM. More cores means
autogives you more workers, which means your FPM pool size probably needs a revisit too. They move together. - When CPU is pegged but requests per second are low. Sometimes I've found a client stack where
worker_processeswas manually set to 1 years ago and nobody noticed. More workers, free throughput.
I would not chase nginx tuning if I haven't already looked at Laravel's query log, checked OPcache is on, and confirmed PHP-FPM pm.max_children is set to something sensible. nginx config is the last 5% of performance on most Laravel stacks, not the first.
The Actual Formula I Use
To make this concrete:
worker_processes = logical CPU cores (or auto)
worker_connections = (pm.max_children × 2) × 1.5, rounded to a power of 2
worker_rlimit_nofile = worker_connections × 2 (minimum)
For a 4-core VM with a 50-process FPM pool:
worker_connections = (50 × 2) × 1.5 = 150 → round up to 256 (fine) or just leave at 1024
worker_rlimit_nofile = at least 2048, but just set 65535 and move on
worker_connections 1024 ends up being right for most stacks not because it's a magic number but because it comfortably exceeds what FPM can actually process concurrently and leaves headroom for queuing.
The math isn't hard. What matters is that you're doing it deliberately instead of copying a Stack Overflow answer from 2014 and wondering why your site falls over at 200 concurrent users.
Get the FPM pool size right first. Then make sure nginx isn't the ceiling below it. Then go fix your N+1 queries.
Need help shipping something like this? Get in touch.