nginx and PHP-FPM at 18,000 Connections
An Ubuntu 22.04 web tier runs nginx 1.24 in front of PHP-FPM 8.2 on eight 4 vCPU/16 GB nodes behind an L4 load balancer. Each node peaks at 18,000 established client connections and 60,000 requests per minute; keepalive to upstreams is enabled. Users report sporadic 502 responses and occasional 30-second stalls. Logs show worker_connections are not enough, Too many open files, and connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource temporarily unavailable). ulimit -n in an interactive root shell shows 65536, while an nginx worker process reports 1024 in /proc//limits. PHP-FPM's pool uses pm=static, pm.max_children=120, and listen backlog 511. The nginx systemd unit sets no LimitNOFILE. fs.file-max is effectively unlimited. The team's first instinct is to double pm.max_children to 240 and restart everything.
1. Map each of the three error messages to the specific limit or queue that causes it. Why would doubling pm.max_children be likely to make things worse rather than better?
Read the worked answer
These are three separate limits, not one bug.
worker_connections are not enough is nginx's per-worker event slot limit. Each proxied HTTP connection consumes two slots (downstream client plus upstream). At 18,000 clients, a node needs roughly 36,000 slots. The default 512 per worker times 4 workers is far below that.
Too many open files is RLIMIT_NOFILE on the worker. The root shell's 65536 comes from PAM/login limits; it is irrelevant to the service. systemd gave the unit the default soft limit of 1024, and nginx inherited it. Once the worker reaches 1024 file descriptors it cannot accept a client or open an upstream socket. Setting worker_connections above the fd limit is meaningless.
connect() ... Resource temporarily unavailable (EAGAIN) to the PHP-FPM unix socket means the listen backlog is full. With pm=static and 120 children, throughput is roughly 120 divided by per-request service time. If each request takes 50 ms, that is about 2,400 req/s, which covers 1,000 req/s - until a database lock or storage stall makes requests slow. Then the 511-entry backlog fills and nginx gets EAGAIN instead of a queued connection.
Doubling pm.max_children to 240 treats the third symptom by adding workers that all contend for the same database and storage. If each child is 60-80 MB RSS, 240 children is 14-19 GB on a 16 GB node, which invites the OOM killer and makes 502s worse. It also does nothing for the nginx fd ceiling or connection slots.
2. Give a corrected configuration plan with concrete values and the commands to apply and verify it, including what must be restarted rather than reloaded.
Read the worked answer
Work from measured budgets.
Measure child memory:
ps -o rss= -C php-fpm8.2 | awk '{s+=$1;n++} END {print s/n/1024, n}'. Check socket counts withss -sandcat /proc/net/sockstat.nginx connection and fd budget. For 18k clients plus 18k upstreams plus headroom:
worker_processes auto;
worker_rlimit_nofile 65536;
events { worker_connections 16384; multi_accept on; }
With 8 workers that is 131,072 total slots. Per worker, 2 x 16,384 plus overhead fits within 65,536 fds.
- systemd must grant the limit at process creation:
# /etc/systemd/system/nginx.service.d/limits.conf
[Service]
LimitNOFILE=131072
Then systemctl daemon-reload && systemctl restart nginx. A reload is not enough; rlimits are inherited when the process is created.
FPM backlog: set
listen.backlog = 65535and verifysysctl net.core.somaxconnis at least that. Keeppm.max_childrenat 120 if RSS times 120 leaves memory headroom; otherwise add nodes instead of oversubscribing RAM. If the DB can only handle 120 concurrent connections, 240 children just moves the queue.Kernel:
net.core.somaxconn=65535,net.ipv4.tcp_max_syn_backlog=65535; if the L4 balancer does NAT, check conntrack as well.fs.file-maxis not the constraint.Validate with
nginx -tandphp-fpm8.2 -t, then restart during a canary. Confirm withgrep 'open files' /proc/$(pgrep -o nginx)/limitsandnginx -T | grep -E 'worker_connections|worker_rlimit_nofile'.
3. How would you validate the change under realistic load without causing a full outage?
Read the worked answer
Canary one node, not the fleet.
- Drain one node from the L4 pool (stop new sessions, let existing ones finish), then apply the systemd drop-in and nginx/FPM configuration and restart.
- Confirm the limits are actually in force:
grep 'open files' /proc/$(pgrep -o nginx)/limits, the same for a worker PID, andnginx -Tforworker_connectionsandworker_rlimit_nofile. - Generate load above production on the canary:
hey -c 20000 -z 3m https://canary/health, or replay production traffic at 120% for 10 minutes. - Watch
ss -s,nstat -az TcpExtListenOverflows TcpExtListenDrops TcpExtTCPAbortOnMemory, nginx stub_status active/reading/writing, and the FPM status page's listen queue and max listen queue. Check error-log deltas for EMFILE and EAGAIN, anddmesgfor OOM or conntrack messages. - Let it run 30 minutes clean, return it to the pool, then roll the remaining nodes in waves with one canary always out of rotation. Keep the old unit file and config in version control so rollback is a single revert plus restart.