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 static or dynamic and normal under ondemand. 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 reached incrementing is a capacity signal only under dynamic or ondemand. Under static the counter is inert; use listen queue depth and the active/idle ratio instead.

Tradeoffs and when to use it

ModeIdle memoryBurst responseCold-start latencymax children reached
staticAlways N workers warmInstantNone at runtimeAlways 0
dynamicScales toward min_spare_serversReactive, timer-boundFork lag during scalingMeaningful
ondemandNear zeroFork per request after idleEvery cold requestMeaningful

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

SignalWhy it mattersWarning sign
Total processesMust match mode expectationsstatic below max_children (workers dying); dynamic stuck at max; ondemand above 0 with no traffic
Idle processesBurst headroom, mode-dependentZero under static or dynamic during traffic; meaningless at zero under ondemand
Active processes / max_childrenUtilization ratioSustained above 80%
Max children reachedScaling intent blockedIncrementing during normal traffic (dynamic/ondemand only)
Listen queueEarliest user-facing saturation signalAny sustained non-zero value
Per-worker RSSCapacity math and leak detectionGrowth 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_children or 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.