PHP-FPM’s ondemand process manager exists to reclaim memory when a pool is doing nothing. The tradeoff is paid in latency on the first request after an idle period: the master must fork a worker, that worker must handle its first request, and if OPcache has no compiled bytecode for the requested scripts, PHP must parse and compile them from disk. On a warm dynamic or static pool, none of that happens. On an ondemand pool that has been idle, all of it happens on the critical path of a single request.

This is not a bug and it is not an outage. It is the designed behavior of the mode. The most common operational mistake is treating the symptoms of ondemand as incidents: paging on total processes = 0, paging on a latency spike that clears in seconds, or chasing the fork cost as if it were a regression. The second most common mistake is deploying ondemand on a workload where the cold-start cost is unavoidable and user-facing, then being surprised that every poll-driven client pays the penalty repeatedly.

What it is and why it matters

In ondemand mode, the pool starts with zero workers. The master process holds the listening socket and waits. When a connection arrives and there is no idle worker to hand it to, the master forks a worker. That worker accepts the connection, initializes PHP, executes the request, returns the response, and then, after pm.process_idle_timeout (default 10s) with no further work, the master kills it.

The steady state of an idle ondemand pool is total processes = 0. This is the single most important thing to internalize. Every monitoring rule, every alert, every dashboard built on the assumption that zero workers means a dead pool will fire constantly against an ondemand pool that is working exactly as designed. See the PHP-FPM idle processes at zero: no burst headroom left guide for the contrast with dynamic and static modes, where zero idle is a genuine warning.

The reason ondemand exists is memory. A static pool of 50 workers holds 50 PHP processes resident whether they are doing work or not. A dynamic pool holds at least pm.min_spare_servers resident. An ondemand pool holds zero when idle. On a host running many small, bursty PHP applications (shared hosting, low-traffic control panels, cron-triggered webhooks), the memory savings are real and large. The cost is that the first request after an idle gap pays a latency penalty that a warm pool does not.

How it works

The cold-start latency on the first request after idle is the sum of three costs that a warm worker avoids entirely.

flowchart TD
    A[Request arrives] --> B{Idle worker?}
    B -- no --> C[Master forks worker]
    C --> D[Worker inits PHP runtime]
    D --> E[OPcache has bytecode?]
    E -- no --> F[Compile scripts from disk]
    F --> G[Execute request]
    E -- yes --> G
    B -- yes --> G
    G --> H[Response + return to idle]
    H --> I{More work within
process_idle_timeout?} I -- yes --> G I -- no --> J[Master kills worker] J --> K[total processes = 0]

1. Fork cost. The master calls fork() to create a new worker process. On modern Linux this is cheap relative to the rest of the stack but not free: page tables are copied, file descriptor tables are duplicated, and the new process must be scheduled. The fork itself is typically the smallest component of cold-start latency, measured in low single-digit milliseconds on an unconstrained host.

2. Request initialization and application bootstrap. The forked worker inherits PHP module state (SAPI, loaded extensions, php.ini settings) from the master via fork(); it does not re-read configuration or reload extensions at fork time. What the first request pays is RINIT plus application bootstrap (autoloader resolution, service container hydration, configuration loading). These per-request costs exist regardless of pool mode, but ondemand pays them on a process that has never served a request. Framework applications with heavy bootstrap paths are most affected.

3. OPcache compile penalty. This is the component that varies the most. OPcache stores compiled bytecode in shared memory managed by the FPM master process, so it persists across worker kills. If the requested scripts are already cached (compiled by a previous worker, with the master still running and the cache not cleared), the cold-start worker skips compilation. If OPcache is empty (master was restarted, cache was cleared on deploy, or scripts were evicted due to opcache.memory_consumption limits), every script in the request path must be parsed and compiled from disk. For a simple script this adds a few milliseconds. For a framework application with hundreds of included files, the compile penalty dominates the cold-start cost and can push the first request into the tens or hundreds of milliseconds.

A critical distinction: after a simple idle period (workers killed, master still running), OPcache is warm and the cold-start penalty is primarily the fork cost plus first-request overhead. After a master restart or deploy, OPcache is cold and the compile penalty applies. The worst case is a burst of traffic arriving immediately after a restart against a zero-worker pool with cold OPcache. The PHP-FPM mental model guide covers this burst-against-cold-pool pattern. Under low traffic the penalty is a single slow request. Under burst traffic where many requests arrive at once against a zero-worker pool, the master must fork many workers simultaneously, each paying the compile cost, and CPU spikes across the pool while throughput stays low. This is the opposite of the normal high-traffic signature, where CPU rises because workers are doing useful work.

Where it shows up in production

The places ondemand cold start shows up as a problem are consistent across deployments.

Intermittent user-facing latency on low-traffic endpoints. A health check, an admin panel, an infrequently used webhook: any endpoint that goes minutes or hours between requests is a candidate for a perceptible cold-start delay. Users report “the first click is slow, then it’s fine.” If the pool is ondemand and process_idle_timeout is short, this is the expected pattern, not a defect.

Monitoring that fires on the wrong signal. The two most common false alarms are total processes = 0 paged as a pool outage, and first-request latency spikes paged as a regression. Both are correct observations of the data and incorrect interpretations of the system. The PHP-FPM active processes near max_children guide covers the adjacent failure of reading saturation metrics without accounting for pm mode.

Polling clients that re-trigger cold starts. The classic example is desktop sync clients or mobile apps that poll every 30 seconds. Each poll, if it arrives after the worker has been killed by process_idle_timeout, pays the full cold-start cost. The pool is never genuinely warm. Several applications with polling-heavy clients document ondemand as unsuitable for exactly this reason.

Burst arrivals against a zero-worker pool. If traffic arrives in tight bursts separated by idle gaps longer than process_idle_timeout, every burst pays cold start. A cron-driven batch that fires 20 concurrent requests at an ondemand pool that has been idle for a minute will fork 20 workers, each compiling from an empty OPcache if the master was recently restarted, and the burst will be visibly slow. A warm pool would absorb the same burst in milliseconds.

Container and cgroup-constrained hosts. On a host under memory pressure or a container with tight CPU limits, the fork and compile costs inflate. What is 10-50ms on an unconstrained host can become hundreds of milliseconds on a throttled container, because the worker initialization contends with cgroup CPU limits and the compile phase does real CPU work. See PHP-FPM in containers: cgroup limits and the silent OOM kill for the adjacent container-specific failure modes.

Tradeoffs and when to use it

Ondemand is a deliberate trade of memory for latency. The decision to use it should be made explicitly, not by default.

Ondemand is worth it when:

  • The pool is genuinely idle for long stretches. Shared hosting, low-traffic control panels, cron-triggered endpoints, internal tooling used a few times a day. Here the memory savings are real and the cold-start cost is paid rarely and by users who tolerate it.
  • Memory is the binding constraint. If the host cannot afford to keep N workers resident, ondemand is the lever that lets you run the pool at all. The alternative is not a warm dynamic pool, it is not running the application.
  • First-request latency is not user-facing. Background jobs, webhooks where the caller has generous timeouts, internal health checks that report liveness rather than latency. In these cases the cold-start cost is invisible to anything that matters.

Ondemand is not worth it when:

  • Traffic is continuous or bursty-with-short-gaps. If the pool would be warm most of the time anyway under dynamic, the memory savings vanish and the cold-start tax remains. Use dynamic with pm.min_spare_servers sized to your burst pattern.
  • Clients poll on short intervals. Mobile and desktop sync clients, health checks from aggressive load balancers, any client that reconnects faster than process_idle_timeout. These clients pay the cold-start cost on every interaction. Either raise process_idle_timeout above the poll interval or switch to dynamic.
  • The application is latency-sensitive and framework-heavy. A Laravel or Symfony application with hundreds of included files pays a large compile penalty when OPcache is cold. After a master restart or deploy, the first wave of requests to an ondemand pool pays full compile cost. For these workloads, dynamic or static with a properly sized OPcache is almost always the better choice.
  • You need predictable p99. Ondemand introduces a bimodal latency distribution: fast when warm, slow when cold. If your SLO is on tail latency, the cold-start tail is hard to eliminate without keeping the pool warm.

Tuning levers if you keep ondemand.

  • pm.process_idle_timeout (default 10s) controls how long a worker stays alive after its last request. Lower values reclaim memory faster but increase cold-start frequency. Values below 10s are almost always wrong because each spawn/kill cycle is the most expensive thing the pool does. Raise it above your typical inter-request gap if clients poll.
  • pm.max_children sets the ceiling on simultaneous cold starts during a burst. If a burst arrives against a zero-worker pool, the master forks up to this many workers at once, each paying compile cost if OPcache is cold. Size it for memory, not for steady-state concurrency.
  • OPcache sizing matters more in ondemand than in dynamic, because a cold pool after restart means a cold cache. Ensure opcache.memory_consumption and opcache.max_accelerated_files are sized for the codebase so the cache survives long enough to benefit subsequent cold starts within a traffic window.
  • Pre-warming the cache before directing traffic (hitting critical endpoints after a restart or during a deploy window) reduces the compile penalty for the first real users. This is the same technique recommended for any cold-start scenario.

A known upstream issue is that ondemand does not always scale down as expected: the idle-child selection algorithm can leave workers alive longer than configured, so a pool may sit at 20-30 workers when you expect zero. If you observe workers accumulating in an ondemand pool that should be idle, this is a known behavior rather than a misconfiguration of process_idle_timeout.

Signals to watch in production

SignalWhy it mattersWarning sign
total processesCore to understanding ondemand. Zero when idle is normal; non-zero is the live worker count, not the configured ceiling.Alerting on zero will page constantly. Alert on the pool being unresponsive to traffic, not on worker count.
listen queueThe earliest signal of real saturation. In ondemand, brief queue during cold start is normal and clears in seconds.Sustained non-zero queue with traffic present means workers are not keeping up, not that cold start is the problem.
active processes / total processes ratioSaturation percentage in ondemand is misleading because total is the live count, not max_children. The ratio can hit 100% with room to scale.Do not use this ratio as a capacity signal in ondemand. Use max children reached and listen queue instead.
First-request latency (application-level)The direct measurement of cold-start cost. Expect bimodal distribution: fast when warm, slow when cold.A flat latency distribution with no cold-start tail suggests the pool is staying warm (good) or you are not sampling the cold requests.
OPcache hit rateConfirms whether cold starts are paying a compile penalty. OPcache persists across worker kills, so low hit rate points to a restart, deploy clear, or cache eviction, not idle.Hit rate that never recovers between bursts means OPcache is too small or being cleared.
max children reached counterMeaningful in ondemand, unlike static. Increments when the pool wanted to fork but hit the ceiling.Rate of change during normal traffic means max_children is too low for the burst pattern.
Fork frequencyEach fork is a cold-start cost. High fork rate with low throughput means workers are being killed and respawned too aggressively.Correlates with process_idle_timeout being too short for the traffic pattern.

The monitoring posture for ondemand is different from dynamic and static. Do not alert on total processes = 0. Do not alert on a single slow first request. Do alert on sustained listen queue > 0, on max children reached climbing, and on the pool failing to accept traffic at all (ping failure with incoming requests). See PHP-FPM listen queue growing: the earliest signal of saturation for the saturation signal that does warrant alerting regardless of pm mode.

How Netdata helps

  • Per-second polling of total processes, active processes, and idle processes shows the ondemand fork and kill cycle directly, including the brief window where workers exist between first request and idle timeout. At coarser polling intervals this behavior is invisible.
  • Anomaly detection on first-request latency distinguishes the expected cold-start tail from a genuine regression. Rather than alerting on every slow first request, it flags only latency that deviates from the established pattern for this pool.
  • Correlating fork events with OPcache hit rate and CPU shows whether cold-start latency is dominated by compile cost (OPcache miss) or by fork overhead. This separates “OPcache is too small” from “the pool is cycling too aggressively.”
  • The PHP-FPM collector exposes max children reached and listen queue as first-class signals, so alerting can target the real saturation indicators rather than the misleading active/total ratio that ondemand distorts.
  • Dashboards that place FPM pool metrics next to web server 502/504 rates and cgroup CPU throttling make it obvious whether a cold-start spike is user-visible (504s from the web server) or absorbed (slow but successful requests).