worker_processes × worker_connections gets quoted as nginx’s capacity, and for a static file server it is roughly right. For anything proxying to PHP-FPM it is out by a factor of two, because a request in flight holds one connection to the client and a second one to the upstream.
worker_processes auto; # one per core
worker_rlimit_nofile 8192; # or the connections cannot be opened at all
events {
worker_connections 2048; # per worker, not in total
multi_accept on;
}
# 4 workers × 2048 = 8192 connections
# ÷ 2 for the upstream leg = 4096 concurrent proxied requests
# minus every keepalive connection sitting idle between requests
Keepalive is the second correction: a browser holds its connection open for keepalive_timeout after the response, so connections are occupied by clients who are not asking for anything, and 75 seconds of that is a great many of them on a page with any think time. The limit that bites first is usually not this one at all — every connection is a file descriptor and the default ulimit -n of 1024 sits below any value worth setting here, which is what worker_rlimit_nofile exists to raise. And none of it matters if PHP-FPM has 25 children, because nginx will cheerfully accept 8,000 connections and queue 7,975 of them, converting a refused connection into a slow one. Size the pool first; use this to make sure nginx is not the thing turning people away.