PHP-FPM’s pm directive controls how the master process decides how many worker processes to keep alive. There are three modes: static, dynamic, and ondemand. The choice changes how your pool reacts to a traffic burst, how much idle memory you pay for, and which status page counters are meaningful.
Each worker handles exactly one request at a time, so worker count is your concurrency ceiling. The pm mode determines whether that ceiling is pre-allocated, scaled up reactively, or created on demand. Pick the wrong mode for your traffic pattern and you get one of three failure shapes: RAM burned on idle workers, latency spikes when the pool cannot fork fast enough, or a pool that scales down to zero and has to cold-start back up at the worst moment.
This guide covers what each mode does under load, how operational signals change meaning depending on which mode you run, and why many experienced operators default to static in production. It assumes you already know the master-worker model and request lifecycle; for that background see How PHP-FPM actually works in production.
The pm directive and why the mode choice matters
The pm directive lives in the pool configuration, for example /etc/php/8.x/fpm/pool.d/www.conf. Every pool picks exactly one mode, and the choice is per-pool. All three modes share pm.max_children as the hard ceiling on worker count. They differ in how and when they approach that ceiling and what they do with workers when traffic subsides.
Forking a worker is not free. A fork copies page tables and initializes the PHP runtime, and the new worker may need to warm up framework state, autoloader caches, and connection pools on its first request. The pm mode determines whether you pay that cost before traffic arrives (static), pay it reactively during traffic (dynamic), or pay it on every cold request after an idle period (ondemand).
Monitoring also changes per mode. A pool sitting at zero idle workers is a red flag under static or dynamic but normal under ondemand. The max children reached counter increments only for dynamic and ondemand; for static it is always zero. Treating the same metric the same way across modes produces false alarms and missed incidents.
How it works
The three modes share the same master and worker model. They differ only in the spawn-and-trim policy the master runs between requests.
flowchart TD
Start["Pool starts or reloads"] --> Mode{pm mode}
Mode -- static --> SA["Fork pm.max_children,
keep all warm"]
Mode -- dynamic --> DA["Fork pm.start_servers,
scale by spare counts"]
Mode -- ondemand --> OA["Fork zero workers"]
SA --> Burst["Traffic arrives,
workers serve"]
DA --> Burst
OA --> Burst
Burst --> Drop["Traffic subsides"]
Drop --> SB["Keep all workers warm"]
Drop --> DB["Trim toward
max_spare_servers"]
Drop --> OB["Kill idle workers after
pm.process_idle_timeout"]static
Fixed worker count. pm.max_children workers are forked at startup and kept alive indefinitely, subject only to pm.max_requests recycling. The process manager never spawns under load and never trims when idle. active processes plus idle processes always equals pm.max_children, which turns total process count into a simple integrity check: anything less means workers died and were not replaced.
Memory is fully predictable. If you size for N workers at peak RSS, that is what the box holds all day, idle or busy. The cost is idle memory: every idle worker is a warm PHP process holding its RSS, doing nothing. Those idle workers are how the pool absorbs a spike with zero fork latency. Treat them as burst absorption capacity, not waste.
max children reached is always 0 in static mode because the pool never attempts to exceed the pre-forked count. Saturation shows up only as listen queue growth and a climbing active/idle ratio.
dynamic
Scales between pm.min_spare_servers and pm.max_children. At startup the master forks pm.start_servers workers. It then periodically checks the idle count and forks new workers when idle falls below min_spare_servers, or trims when idle exceeds max_spare_servers.
The critical limitation is that scaling is reactive, not predictive. The master checks spare counts on a timer, then forks workers one batch at a time. Under a sudden burst, the timer may not fire fast enough to spawn workers before the listen queue builds. This is the silent-starvation shape: the pool is configured to scale, but the scaling lag is long enough that requests queue anyway.
When the master falls behind it logs a “seems busy” notice and spawns workers in a batch. Before PHP 8.1 that batch size was hardcoded; PHP 8.1 exposes it as pm.max_spawn_rate with a default of 32. A large spawn batch under memory pressure can itself cause trouble, since dozens of fresh workers each warm up their own RSS at once.
The default dynamic settings shipped by most distributions are conservative and often too low for real traffic. pm.min_spare_servers in particular is usually set low enough that a burst drains the spare pool before the timer spawns replacements.
ondemand
Zero workers at startup and during idle periods. The master spawns a worker when a request arrives, the worker serves it, and after pm.process_idle_timeout seconds of idleness the worker is killed. Zero workers at idle is the designed behavior, not a failure.
The trade is memory for latency. On a consistently busy server, ondemand is a poor fit: every traffic dip kills workers, and every traffic return pays fork and warmup latency to respawn them at the worst moment. For low-traffic sites, cron-triggered pools, admin panels, or staging environments that go genuinely idle for long stretches, ondemand keeps idle memory near zero.
Cold-start latency is the signature cost. The fork penalty runs roughly 10-50ms extra per cold request, potentially higher on resource-constrained hosts or heavy framework apps. The listen queue may briefly show non-zero depth during cold starts, which is expected and should clear within seconds.
Where it shows up in production
Most production PHP-FPM deployments run dynamic because that is the distribution default. That does not make it the right choice. Two patterns push operators away from dynamic: scaling lag during bursts, and “seems busy” spawn batches that spike memory.
One pattern pushes operators away from ondemand: repeated cold-start latency on busy servers. Ondemand earns its place on pools with long idle periods where paying zero idle memory matters more than absorbing a burst instantly.
One pattern pushes operators toward static: steady traffic plus bursts that the reactive scaler cannot keep up with. With static there is no scaler to lag. The workers are already there.
The monitoring implications differ per mode in ways that catch teams off guard:
- Zero idle workers is an incident under
staticordynamicand normal underondemand. Alert thresholds must be mode-aware. - Zero total workers with active traffic is broken in any mode. Zero total workers with no traffic is correct only under
ondemand. max children reachedincrementing is a capacity signal only underdynamicorondemand. Understaticthe counter is inert; use listen queue depth and the active/idle ratio instead.
Tradeoffs and when to use it
| Mode | Idle memory | Burst response | Cold-start latency | max children reached |
|---|---|---|---|---|
| static | Always N workers warm | Instant | None at runtime | Always 0 |
| dynamic | Scales toward min_spare_servers | Reactive, timer-bound | Fork lag during scaling | Meaningful |
| ondemand | Near zero | Fork per request after idle | Every cold request | Meaningful |
When to pick static. If your traffic is steady or bursts frequently, and the box has the memory to hold N warm workers all the time, static removes the scaler as a failure surface. Many experienced operators run static in production precisely because it eliminates scaling latency at the cost of constant memory usage. Size pm.max_children from memory, not CPU. PHP-FPM workers spend most of their time blocked on I/O, so a 4-core box can often run dozens of workers if memory allows.
When to pick dynamic. If traffic is variable with real idle gaps and you cannot afford to hold a full peak worker set warm around the clock, dynamic trims workers during lulls. The catch is that you must tune the spare settings for your actual burst pattern. Set pm.min_spare_servers high enough to absorb the bursts you expect between timer ticks. If the “seems busy” notice fires regularly, your spare settings or max_children are too low for the traffic.
When to pick ondemand. If the pool goes genuinely idle for long periods and cold-start latency is acceptable, ondemand keeps idle memory near zero. Avoid it for consistently busy servers. Also avoid it behind synchronous health checks that expect instant responses, since the first check after an idle period pays the fork penalty.
Switching modes. Changing pm requires a graceful reload (SIGUSR2 or systemctl reload php-fpm) or restart. During a graceful reload the master re-execs and new workers spawn against the new config while old workers drain current requests. Expect a momentary listen-queue blip during the re-exec, not a hard outage.
Signals to watch in production
| Signal | Why it matters | Warning sign |
|---|---|---|
| Total processes | Must match mode expectations | static below max_children (workers dying); dynamic stuck at max; ondemand above 0 with no traffic |
| Idle processes | Burst headroom, mode-dependent | Zero under static or dynamic during traffic; meaningless at zero under ondemand |
| Active processes / max_children | Utilization ratio | Sustained above 80% |
| Max children reached | Scaling intent blocked | Incrementing during normal traffic (dynamic/ondemand only) |
| Listen queue | Earliest user-facing saturation signal | Any sustained non-zero value |
| Per-worker RSS | Capacity math and leak detection | Growth between restarts with no recycling |
How Netdata helps
- Per-second polling of the FPM status page catches transient listen queue build-ups that 10-30 second intervals miss entirely. PHP-FPM saturation events unfold in seconds.
- Correlating pool metrics (active, idle, total processes) with host and cgroup memory shows whether a dynamic “seems busy” spawn batch is about to hit a memory ceiling before workers start dying.
- Mode-aware baselining through ML anomaly detection learns the expected process-count shape per pool, so a static pool dropping below
max_childrenor an ondemand pool failing to spawn under traffic stands out without a hand-tuned threshold. - Showing the active/idle ratio alongside listen queue depth makes the static advantage visible: a properly sized static pool holds a flat worker count with the queue at zero through a burst.
- Per-pool dashboards keep multiple pools separate, so a leak or saturation in one pool is not averaged away across the host.
Related guides
- PHP-FPM 504 Gateway Timeout: requests accepted but never finishing in time
- PHP-FPM active processes near max_children: reading pool utilization
- PHP-FPM in containers: cgroup limits and the silent OOM kill
- PHP-FPM “child N exited on signal 11 (SIGSEGV)”: worker segfaults
- PHP-FPM crash loop and fork storm: workers dying faster than they serve
- PHP-FPM emergency restart: “failed processes threshold reached, initiating reload”
- How PHP-FPM actually works in production: a mental model for operators
- PHP-FPM idle processes at zero: no burst headroom left
- PHP-FPM listen queue growing: the earliest signal of saturation
- PHP-FPM “server reached pm.max_children setting (N), consider raising it”
- PHP-FPM pm.max_requests: worker recycling as the memory-leak safety net
- PHP-FPM memory leak: per-worker RSS climbing until the box runs out






