The PHP-FPM status page reports idle processes at or near zero. In static and dynamic modes this is the leading indicator that the pool is about to start queuing. The next request that arrives has no worker ready to accept it, so it lands in the kernel-managed socket backlog. If demand keeps rising, the backlog fills and the web server starts returning 502s.
Zero idle is not itself a failure but the warning that one burst or one slow dependency will turn into visible user impact within seconds. Once the listen queue is non-zero, users are already waiting on extra latency. Once the backlog overflows, you are dropping connections that PHP-FPM cannot see.
What this means
Idle workers are the reserve that absorbs bursts with zero latency. A request that arrives when an idle worker exists is dispatched immediately. A request that arrives when every worker is busy must wait in the socket backlog (listen.backlog). The backlog adds latency, and once it fills, the kernel drops new connections before PHP-FPM ever sees them.
The interpretation of zero idle depends on the process manager mode.
| Mode | What zero idle means | Is it a problem? |
|---|---|---|
static | All pm.max_children workers are busy. No scaling is possible. | Yes, if sustained. The pool is at ceiling. |
dynamic | All workers are busy AND the scaler cannot fork more because max_children is reached. Idle pinned at pm.min_spare_servers is the under-provisioning signature. | Yes, if sustained or if idle is stuck at min_spare_servers. |
ondemand | All spawned workers are busy. Zero idle at rest (no traffic) is the designed behavior. | Only under load. At rest it is normal. |
The target operating band is 20-30% of max_children idle during peak traffic. Below that, a single slow dependency or a modest traffic spike pushes the pool into queuing. Above that, you are trading memory for headroom, which is usually the right trade for a request-per-worker model where each worker handles exactly one request at a time.
The cascade from zero idle to user-visible errors is short and deterministic.
flowchart TD
A["idle near 0"] --> B{"mode and load?"}
B -->|"ondemand at rest"| C["normal: no action"]
B -->|"static/dynamic under load"| D["active at max_children"]
D --> E{"listen queue and durations?"}
E -->|"queue 0, durations normal"| F["under-provisioned"]
E -->|"queue > 0"| G["queuing: add capacity"]
E -->|"durations bimodal"| H["slow dep: fix upstream"]
G --> I["kernel drops if backlog fills"]The branch at listen queue and durations? is the diagnostic fork: raise max_children or chase a slow backend. Get that branch right once you have confirmed zero idle is sustained.
Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Pool under-provisioned | Idle pinned near 0, active = max_children, listen queue 0 or small, request durations normal, slow log quiet | max children reached counter rate; compare peak active to max_children |
| Slow dependency draining workers | Idle near 0, active climbing, request durations bimodal (some fast, some very long), slow log filling | Slow log stack traces; per-worker request duration and request uri |
| Memory ceiling forcing low max_children | Idle near 0, max_children set conservatively, per-worker RSS high or growing | avg_RSS * max_children vs available RAM; leak check if pm.max_requests = 0 |
| Traffic spike beyond provisioned capacity | Idle drops fast, active hits ceiling, accepted conn rate spikes, all request types affected equally | Web server access log rate vs baseline |
| ondemand cold-start or idle timeout | Zero idle at rest, workers spawn on first request, brief listen queue during fork | Traffic pattern; pm.process_idle_timeout value |
| Workers stuck and not returning to idle | Idle near 0, some workers show very long request duration, request_terminate_timeout unset or too high | Per-worker full status; check for hung external calls |
Quick checks
These are read-only. Run them in this order.
# Idle, active, total, listen queue, high-water marks from the status page
curl -s http://127.0.0.1/fpm-status | grep -E "^(idle|active|total|listen queue|max children|max active)"
# Confirm mode and configured ceilings before interpreting anything
grep -E "^[[:space:]]*pm" /etc/php/*/fpm/pool.d/*.conf
# Is the scaler blocked by max_children? (dynamic/ondemand only; always 0 in static)
curl -s http://127.0.0.1/fpm-status | grep "max children reached"
# Per-worker view: which requests are holding workers, and for how long
curl -s 'http://127.0.0.1/fpm-status?full' | grep -E "request duration|request uri|state"
# Kernel-level listen backlog depth (Recv-Q on the LISTEN line)
ss -xlnp | grep php # Unix socket
ss -tlnp | grep 9000 # TCP socket
# Recent slow log entries - the most diagnostic signal available, if configured
tail -50 /var/log/php-fpm/slow.log 2>/dev/null || tail -50 /var/log/php-fpm/www-slow.log 2>/dev/null
# Worker RSS to check the memory ceiling
ps -eo pid,rss,cmd | grep '[p]hp-fpm' | grep -v master | sort -nk2 | tail
# Accepted connection rate (poll twice, divide delta by interval)
curl -s http://127.0.0.1/fpm-status | grep "^accepted conn"
# Kernel listen overflow/drop counters - invisible to FPM itself
nstat -z TcpExtListenOverflows TcpExtListenDrops
If request_slowlog_timeout is not configured, the slow log check returns nothing useful. Enable it before the next incident. You cannot diagnose slow-request contagion without it.
How to diagnose it
Confirm the mode. Zero idle means different things in
static,dynamic, andondemand. Readpm =from the pool config before interpreting anything else. Inondemandat rest, zero idle is correct behavior and you can stop here.Establish whether zero idle is sustained or transient. Poll the status page at 1-second intervals during the symptom. A 10-second poll will miss the entire cascade. The status page is a point-in-time snapshot, not an average.
Correlate idle with active and listen queue. The signature of real trouble is
active = max_childrenANDlisten queue > 0. Zero idle alone, with empty queue and normal durations, is tight but stable. The listen queue appearing is the line between “headroom is thin” and “users are waiting”.Read
max children reached. Indynamicandondemandmodes this counter increments each time the scaler wanted to fork but could not. A climbing rate is direct evidence of under-provisioning. Instaticmode this counter is always 0 because workers are pre-forked; usemax active processesinstead.Pull the full status and sort workers by
request duration. Bimodal distribution (some fast, some extremely slow) points to slow-request contagion, not capacity. Uniformly elevated durations point to a systemic problem (opcache thrash, CPU saturation).request durationis in microseconds; 1,000,000 is one second.Check the slow log. If
request_slowlog_timeoutis configured, the stack traces tell you which call is blocking workers. A trace pointing at PDO or mysqli is a database problem. A trace pointing at curl or stream functions is an external API problem. A trace pointing atsession_startis session lock contention.Check per-worker RSS against the memory ceiling. If
avg_RSS * max_childrenis already near available RAM, raisingmax_childrenis unsafe and you must reduce per-worker memory first. Use PSS (viasmemor/proc/<pid>/smaps_rollup), not RSS, for the calculation. RSS overstates unique memory because it counts shared opcache pages in each worker.Check kernel-level drop counters. If
TcpExtListenOverflowsis increasing, connections are already being dropped at the kernel level and users are seeing 502s that PHP-FPM cannot account for.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
idle processes | The reserve that absorbs bursts. Leading indicator before queuing. | Sustained below 20% of max_children during peak; pinned at min_spare_servers in dynamic mode |
active processes | Current concurrency. The numerator in utilization. | Sustained above 80% of max_children |
listen queue | Requests waiting for a worker. Any sustained non-zero value means saturation. | Any non-zero value sustained more than a few seconds |
max children reached | Counter of times the scaler was blocked by max_children. Direct under-provisioning evidence. | Any non-zero rate during normal traffic (dynamic/ondemand only) |
max active processes | High-water mark of concurrent active workers. | Equal to max_children means the wall was hit at some point |
Per-worker request duration | Distribution of request times. Identifies slow-request contagion. | Multiple workers above 10x median |
| Slow log entries | Stack traces of blocked workers. Most diagnostic signal available. | Any sustained non-zero rate |
| Per-worker RSS | Memory per worker. The real ceiling on max_children. | Monotonic growth between restarts with pm.max_requests = 0 |
TcpExtListenOverflows | Kernel-level connection drops invisible to FPM. | Any non-zero rate |
Fixes
If the pool is under-provisioned
Raise pm.max_children, but only after checking the memory ceiling. The binding constraint on max_children is RAM, not CPU. PHP-FPM workers spend most of their time waiting on I/O, so a 4-core box can often run dozens of workers if memory allows.
Formula: memory_safe_max_children = (total_RAM * 0.7 - OS_overhead) / avg_worker_PSS. Use PSS, not RSS, for the divisor. A 60MB-RSS worker might have a PSS of 35MB once shared pages are accounted for.
In dynamic mode, also raise pm.min_spare_servers and pm.start_servers so the scaler keeps more idle workers on hand. The scaler is reactive, not predictive. It checks spare counts on a timer and forks workers at a bounded rate. Under burst, it cannot keep up if min_spare is too low, and idle hits zero before the new workers are ready.
Apply the change with a graceful reload (systemctl reload php-fpm or kill -USR2 $(cat /run/php-fpm.pid)). During reload, the master spawns new workers with the updated config and signals old workers to finish their current requests and exit. Avoid reloading during an active incident; the transition briefly increases memory usage as old and new workers overlap.
If slow requests are draining the pool
Increasing max_children will not help. New workers will hit the same slow dependency and get stuck too. The fix is upstream.
Read the slow log to identify the blocking call. Address the dependency directly: add the missing index, lower the external API timeout, call session_write_close() early in the request, switch file-based sessions to Redis or Memcached. The PHP-FPM pool is the victim here, not the cause.
For immediate relief during an incident, you can kill blocked workers individually with kill -SIGQUIT <pid>. SIGQUIT lets the worker finish its current request before exiting, which is safer than SIGKILL. If a worker is stuck in a blocking C extension call, SIGQUIT may not take effect until the call returns; request_terminate_timeout is the reliable mechanism for truly hung workers. If a single endpoint is the culprit, block it at the web server layer to protect the rest of the site while you fix the backend.
If the memory ceiling forces a low max_children
The real problem is per-worker memory, not worker count. Two paths.
Set pm.max_requests to 500-1000. This forces periodic worker recycling and caps leak growth. The cost is negligible (one fork per N requests) and leaving it at 0 is a common PHP-FPM misconfiguration. Every PHP process leaks something over time; if not your code, then an extension or the runtime itself.
Investigate the leak. Compare RSS of workers with different request counts. Use memory_get_usage() and memory_get_peak_usage() in application code to isolate which request types leak. Extension memory (ImageMagick, libxml) is not bound by PHP memory_limit and is a common source of growth that memory_limit cannot prevent.
If it is ondemand at rest
Nothing to fix. Zero idle at rest is the designed behavior. Workers are killed after pm.process_idle_timeout (default 10s). The tradeoff is memory savings against cold-start latency on the next request, which includes fork cost and opcache warmup.
If you see zero idle under sustained load in ondemand mode, the diagnosis is the same as dynamic mode: check max children reached, the slow log, and per-worker durations. The mode changes the at-rest baseline, not the under-load behavior.
Prevention
- Set
pm.max_requeststo 500-1000 on every pool. Safety net against unbounded memory growth and the cheapest leak insurance available. - Enable
request_slowlog_timeout(e.g., 5 seconds) on every production pool. Without it, you know that something is slow but not what. The slow log is the only signal that names the blocking call. - Set
request_terminate_timeout(e.g., 30-60 seconds). Prevents a single stuck request from permanently removing a worker from the pool. - Poll the status page at 1-second intervals for operational alerting. Saturation events unfold in seconds; a 10-second poll will miss the cascade and only show you the aftermath.
- Track peak active worker counts weekly. If peak active is 80% of max_children and growing 15% month over month, you have weeks of runway, not months.
- Target 20-30% of max_children idle during peak. This is the burst absorption band. Below it, one slow dependency tips you into queuing.
- Monitor kernel-level
TcpExtListenOverflowsalongside the FPM status page. PHP-FPM cannot see connections dropped at the kernel level when the backlog overflows. - Size max_children from memory, not CPU. The formula is
available_memory / avg_worker_PSS. CPU-core-based sizing leaves capacity on the table for I/O-bound workloads. - In
dynamicmode, setpm.min_spare_servershigh enough to absorb your expected burst pattern. The scaler is reactive; min_spare is your burst buffer.
How Netdata helps
- Per-second collection of
idle processes,active processes,total processes, andlisten queuefrom the PHP-FPM status page, so the zero-idle state and the subsequent queue formation land on the same timeline instead of being averaged away by a coarse poll interval. - Anomaly detection on the idle worker series flags the transition from “tight but stable” to “sustained zero” without requiring a static threshold that ignores mode and traffic pattern.
- Correlation of idle workers with per-worker request duration, slow log rate, and per-worker RSS in a single view, which collapses the branch between “under-provisioned” and “slow dependency draining the pool” to seconds rather than minutes.
- Composite alerting on
active = max_children AND listen queue > 0catches the confirmed-queuing signature that either signal alone misses. - Kernel-level socket counters (
TcpExtListenOverflows,TcpExtListenDrops) collected alongside FPM metrics, exposing connection drops that PHP-FPM cannot see in its own status page. - Per-pool dashboards for multi-pool deployments, so a leak or saturation in one pool is not hidden by aggregate metrics across all pools.






