A ticket-sales PHP site runs on a 4 GB server under PHP-FPM; how do you size pm.max_children from worker memory?
answer
- RAM left for PHP, not total RAM
- measure real workers under load
- shared memory inflates RSS
- memory_limit is a worst case
- check CPU and database limits too
basics
~20 sSubtract what the OS, web server, database and other services need, then divide the RAM left for PHP by the measured memory of a busy worker. On 4 GB with about 2.5 GB for PHP and 60 MB per worker, that is roughly 40.
solid answer
~50 s`pm.max_children` must be set so that all workers at once fit in RAM, because each worker handles one request and a sale spike will fill every one. First reserve memory for everything else on the box: the OS and page cache, nginx, maybe a database or cache server. Then measure real worker memory under realistic load, not the idle size, using proportional memory (PSS) or RSS minus shared pages, since OPcache's shared segment appears in every worker's RSS. Divide: on a 4 GB server with about 2.5 GB left for PHP and workers averaging 60 MB, around 40 workers fit. `memory_limit` (128M by default) gives a safe upper bound — 2.5 GB / 128 MB is about 19 — useful when a few requests are very heavy. Finally check that the CPU cores and the database's connection limit can serve that many concurrent requests.
code
bash · 4 lines# average proportional memory (PSS) of busy FPM workers, in MB
for pid in $(pgrep -f 'pool tickets'); do
awk '/^Pss:/ {print $2}' /proc/$pid/smaps_rollup
done | awk '{sum += $1; n++} END {printf "%d workers, %.0f MB avg\n", n, sum/n/1024}'go deeper
Recall that each FPM worker handles one request and uses its own memory, so pm.max_children must fit in RAM.
Walk through the calculation: memory left for PHP divided by measured busy-worker memory, with memory_limit as the worst case.
Show measurement discipline: PSS instead of RSS, busy and aged workers, colocated services reserved, and CPU and database limits checked before raising the cap.
Decide whether heavy endpoints get their own pool, how much headroom to keep for spikes, and when to add servers instead of workers.
## Why memory decides pm.max_children In PHP-FPM each worker process handles exactly one request at a time, and `pm.max_children` caps how many workers a pool may have alive. On a ticket-sales site, the moment tickets go on sale every worker becomes busy at once. If the cap is higher than memory allows, the server starts **swapping** (every request slows down) or the kernel's out-of-memory killer ends processes — possibly the database. If the cap is too low, requests **queue** behind busy workers. So the cap is a memory budget first, and a concurrency target second. ## Step 1: find the memory that is really available to PHP Start from the 4 GB and subtract everything that is not a PHP worker: | Consumer | Example reservation | |---|---| | Kernel, system services, page cache headroom | 500 MB | | nginx and the FPM master | 100 MB | | Database or cache server on the same host | 800 MB | | Deploy tools, cron jobs, a CLI worker | 100 MB | | **Left for FPM workers** | **about 2.5 GB** | These numbers are illustrative; the point is that a colocated database or cache often takes more than people expect, and CLI scripts and queue consumers run outside FPM's cap. ## Step 2: measure a busy worker Worker memory depends on the application, not on FPM. Measure it on a realistic load (a replayed traffic sample or a staging load test that hits the heaviest pages, such as seat selection and checkout): - **RSS** (resident set size, e.g. from `ps -o rss`) **over-counts**: pages shared between workers — the PHP binary, extensions, and **OPcache's shared memory** (`opcache.memory_consumption`, 128 MB by default) — show up in every worker's RSS but exist once. - **PSS** (proportional set size, from `/proc/<pid>/smaps_rollup`) divides shared pages among the processes that use them, so summing PSS over workers gives an honest total. - Use the **high end** of the distribution — for example the 90th–95th percentile of busy workers — not the idle size of a freshly forked worker, which is much smaller. Workers also grow over their lifetime when code keeps caches in static properties or extensions leak; measure workers that have served many requests. ## Step 3: divide, then sanity-check against memory_limit `pm.max_children ≈ memory for workers / per-worker memory` With 2.5 GB and 60 MB per busy worker: 2500 / 60 ≈ 41, so about **40**. Then compare with the **worst case**: `memory_limit` (default `128M`) is the most one request may allocate, so 2.5 GB / 128 MB ≈ 19 workers can never exceed memory even if every request hits the limit at once. Most teams pick a value between the measured figure and this worst case, closer to the measured one, and rely on monitoring. If a handful of endpoints (PDF tickets, report exports) are much heavier, consider moving them to a separate pool with its own lower cap. ## Step 4: check the other bottlenecks Memory sets the ceiling, but more workers are only useful if something can serve them: 1. **CPU.** Requests that are mostly PHP computation cannot run faster than the cores allow; 40 workers on 2 cores just time-share. Requests that mostly wait on the database or HTTP calls can use many more workers than cores. 2. **Database connections.** Every busy worker may hold a connection; 40 workers across several app servers must fit the database's connection limit. 3. **Downstream rate limits** such as a payment provider. ## Step 5: choose the style and verify For a dedicated ticket-sales server with sharp spikes, `pm = static` with the computed value avoids spawn delays during the on-sale moment; `pm = dynamic` with a generous `pm.min_spare_servers` is the alternative when the box does other work outside sales. Afterwards, watch memory and the pool's listen queue during a real peak. A dynamic pool also logs a warning when it reaches `pm.max_children` (a static pool never does, since it is always at its cap); either signal means requests were queuing, and free memory at that moment tells you whether raising the cap is safe.
- Why not simply divide RAM by memory_limit?`memory_limit` is a per-request ceiling, not typical use. Dividing by it assumes every worker hits the limit at once, which is safe but usually leaves most memory unused and caps concurrency far below what the server could handle. Use it as a worst-case bound and size from measured busy-worker memory instead.
- Your RSS-based calculation says only 15 workers fit, yet the server has plenty of free memory with 30. Why?RSS counts shared pages — the PHP binary, extensions and OPcache's shared memory segment — in every worker, although they exist once. Summing RSS over workers therefore over-counts. PSS, which splits shared pages among the processes using them, gives a realistic total.
saying these in an interview costs you the question
- Set pm.max_children from the number of CPU cores alone
- Divide total RAM by worker size without reserving memory for anything else
- Measure a freshly started, idle worker and use that size
- Summed RSS of all workers is the true memory use
- More workers always means more throughput