skip to content

In a PHP-FPM pool with pm = dynamic, how do pm.start_servers, pm.min_spare_servers, pm.max_spare_servers and pm.max_spawn_rate work together?

level: middleimportance: should knowfreq 32%

answer

  1. start, then keep idle within a band
  2. checked about once a second
  3. one idle worker killed per pass
  4. spawn rate doubles, capped at 32
  5. invalid combinations stop FPM

basics

~10 s

A dynamic PHP-FPM pool starts pm.start_servers workers, then once a second kills one idle worker above pm.max_spare_servers or forks more below pm.min_spare_servers, doubling the spawn rate up to pm.max_spawn_rate (32), never past pm.max_children.

solid answer

~40 s

With `pm = dynamic` the master forks `pm.start_servers` workers at startup; if it is unset it uses `min_spare + (max_spare - min_spare) / 2`. About once a second it counts idle workers. Above `pm.max_spare_servers`, it kills one idle worker per pass, so shrinking is gradual. Below `pm.min_spare_servers`, it forks workers, one in the first pass, then doubling each second the shortage lasts, up to `pm.max_spawn_rate` (default 32, added in PHP 8.1), never more than the shortfall below the minimum and never beyond `pm.max_children`. At a spawn rate of 8 or more it logs a 'seems busy' warning. FPM refuses to start when the numbers are inconsistent: spare values must be positive, not above `pm.max_children`, max spare not below min spare, and `start_servers` between them. Tune the spare band to the size of your bursts.

code

ini · 10 lines
ini
[app]
pm = dynamic
pm.max_children = 50
; normal load needs about 15 workers
pm.start_servers = 15
; absorb bursts of about 10 requests without waiting for forks
pm.min_spare_servers = 10
; keep up to 20 idle before trimming one per second
pm.max_spare_servers = 20
; pm.max_spawn_rate = 32 (default)

go deeper

for a junior

Recall that a dynamic pool starts pm.start_servers workers and keeps idle workers between pm.min_spare_servers and pm.max_spare_servers.

for a middle

Explain the once-a-second maintenance pass: one kill per pass above the band, doubling spawns below it, capped by pm.max_spawn_rate and pm.max_children.

for a senior

Size the spare band from burst size, read the seems busy and max_children warnings correctly, and pre-warm or switch to static before planned spikes.

for a principal

Decide whether dynamic's elasticity is worth its spawn lag for a given service or whether committed static capacity is the better contract.

## The idea: keep a band of idle workers A **dynamic** PHP-FPM pool tries to keep a few **idle** (spare) workers ready, so that a new request finds a worker without waiting for a fork, while not holding the peak number of workers all day. Four settings define the band and how fast the pool moves within it, and `pm.max_children` is the ceiling over everything. | Directive | Role | Default / sample | |---|---|---| | `pm.start_servers` | Workers forked at startup | Unset → `min_spare + (max_spare - min_spare) / 2`; sample 2 | | `pm.min_spare_servers` | Fewer idle than this → fork | Mandatory for dynamic; sample 1 | | `pm.max_spare_servers` | More idle than this → kill | Mandatory for dynamic; sample 3 | | `pm.max_spawn_rate` | Cap on forks per pass | 32 (since PHP 8.1) | | `pm.max_children` | Absolute cap on workers | Mandatory; sample 5 | (The "sample" values come from the `www.conf` shipped with PHP and suit only a tiny machine.) ## The maintenance loop The FPM master runs an idle-server maintenance pass about **once per second** for each dynamic pool: 1. It counts idle and active workers. 2. **Too many idle** (`idle > pm.max_spare_servers`): it kills **one** idle worker — the one that has existed longest among the idle ones — and resets its spawn rate to 1. Shrinking therefore happens one worker per second. 3. **Too few idle** (`idle < pm.min_spare_servers`): - if the pool is already at `pm.max_children`, it logs `server reached pm.max_children setting (N), consider raising it` (once until the condition clears) and does nothing else; - otherwise it forks `min(spawn rate, min_spare - idle)` workers, never exceeding `pm.max_children`, and **doubles** the spawn rate for the next pass while it is below `pm.max_spawn_rate`. 4. **Within the band**: nothing happens and the spawn rate goes back to 1. When the spawn rate has climbed to **8 or more**, the master logs: ```text WARNING: [pool app] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 8 children, there are 0 idle, and 23 total children ``` That warning is about **spawning speed**, not the cap: the pool keeps running out of idle workers faster than it can create them. ## Startup validation FPM checks the combination when it starts and refuses to start the pool with an ALERT when it is inconsistent: - `pm.max_children` must be positive. - `pm.min_spare_servers` and `pm.max_spare_servers` must be positive and not greater than `pm.max_children`. - `pm.max_spare_servers` must not be less than `pm.min_spare_servers`. - `pm.start_servers` must lie between the two spare values; if it is unset, FPM computes it and logs a NOTICE. - `pm.max_spawn_rate` must be positive. A common mistake is raising `pm.max_children` to 50 while leaving `start_servers = 2` and `max_spare_servers = 3`: FPM starts, but with `pm.min_spare_servers = 1` the pool only ever forks enough to restore one idle worker, so it grows from 2 workers by one per second at most when traffic arrives, and trims back to 3 idle workers one per second afterwards. ## Checking a configuration before reloading `php-fpm -t` (the binary name varies by distribution, for example `php-fpm8.5`) tests the configuration and exits, printing the same ALERTs a real start would; `php-fpm -tt` additionally dumps the effective values of every pool, including a computed `pm.start_servers`. Running it in the deploy pipeline catches an inconsistent spare band before a reload takes the pool down. ## Tuning the band - **`pm.min_spare_servers`** is the burst you can absorb **instantly**. If requests arrive in bursts of 10, a minimum of 2 means 8 of them wait for spawning. - **`pm.max_spare_servers`** sets how much idle capacity you keep after a burst; too close to the minimum causes churn (kill, fork, kill) on noisy traffic. - **`pm.start_servers`** matters mostly right after a restart or deploy; start near expected normal load. - **`pm.max_spawn_rate`** rarely needs changing; lower it only if a fork storm would hurt the host. For a site with predictable spikes, raise `pm.min_spare_servers` before the event, or move to `pm = static` so no spawning happens at all.

  • FPM refuses to start with 'pm.start_servers(25) must not be less than pm.min_spare_servers(5) and not greater than pm.max_spare_servers(20)'. What is wrong?
    In a dynamic pool, `pm.start_servers` must lie within the spare band. Here 25 is above `pm.max_spare_servers` (20), so the pool would begin by killing workers it just forked; FPM rejects it. Lower `start_servers` to 20 or less, or raise `max_spare_servers`.
  • After a traffic spike, a dynamic pool takes about a minute to drop from 60 to 20 workers. Is something stuck?
    No. When idle workers exceed `pm.max_spare_servers`, the maintenance pass kills only one idle worker per run, about once a second. Trimming 40 surplus workers therefore takes roughly 40 seconds or more, which is intended to avoid churn if traffic returns.

saying these in an interview costs you the question

  • pm.start_servers is the minimum number of workers the pool keeps
  • A dynamic pool forks all missing workers at once when idle ones run short
  • Excess idle workers are all killed immediately
  • pm.min_spare_servers is the minimum total number of workers
  • The seems busy warning means the pool hit pm.max_children