PHP-FPM uses a master-worker process model where each worker handles exactly one request at a time. Concurrent request capacity equals the active worker count, and the path from healthy to users-seeing-502 runs through worker slots, the socket backlog, and kernel connection drops. This is a four-level reference, from minimum liveness checks to the expert signals that explain phantom workers and cgroup OOM kills. Use it as an audit: read down the levels, mark which signals you already collect, and fill the first gap. Levels are cumulative: Level 2 assumes Level 1, Level 3 assumes Level 2.
The common failure is stopping at Level 1 or 2. The status page reads green while the kernel silently drops connections and workers leak memory. The higher levels exist to catch failure modes the lower levels cannot see.
| Level | Question it answers | First symptom it catches |
|---|---|---|
| 1 Survival | Is FPM alive and serving PHP? | Total outage, 502/504 floods |
| 2 Operational | Is the pool healthy under load? | Slow dependencies, memory leaks |
| 3 Mature | Will we saturate soon, and where? | Kernel connection drops, bimodal latency |
| 4 Expert | Why do workers misbehave? | Phantom workers, cgroup OOM, fork stalls |
flowchart TD L1["Level 1 Survival
master, ping, listen queue, 502 rate, memory"] L2["Level 2 Operational
idle workers, RSS, slow log, OPcache, deaths"] L3["Level 3 Mature
latency distribution, kernel drops, patterns, per-pool"] L4["Level 4 Expert
fork latency, PSS, phantom workers, cgroup OOM"] L1 --> L2 --> L3 --> L4
Level 1: Survival
The floor. You can answer “is PHP-FPM up?” and catch hard outages. If you currently monitor nothing else, monitor these five signals.
| Signal | What it tells you | Warning sign |
|---|---|---|
| Master process / ping endpoint | Master is alive and a worker can answer a trivial request | Ping unreachable for more than 2 min with real traffic |
| Active processes | Current concurrency vs pm.max_children | Sustained at 100% of pm.max_children |
| Listen queue depth | Requests waiting because no idle worker exists | Any sustained non-zero value |
| Web server 502/504 rate | Users are seeing errors | 502 = connection refused; 504 = timeout |
| System memory | Is an OOM kill approaching | Free memory declining toward zero |
# Survival checks
curl -sf http://127.0.0.1/fpm-ping || echo "DOWN"
curl -s http://127.0.0.1/fpm-status | grep -E "^(active processes|listen queue)"
ss -lxnt | grep -i php # socket is listening (TCP or Unix domain; may need root for -p)
This is a floor, not a destination. The ping endpoint is handled by a worker but returns a configured string without executing application PHP code, so a successful ping proves a worker can accept a connection but not that your application works. 502/504 rates are trailing signals: by the time they climb, the kernel has already been refusing connections for seconds. The two most common PHP-FPM failure modes (slow dependency drain and memory leak) look completely healthy at this level.
Level 2: Operational
You can tell whether the pool is healthy under load, detect leaks, and see throughput. This is the professional minimum. Missing any signal here leaves you blind to a common failure mode.
| Signal | What it tells you | Warning sign |
|---|---|---|
| Idle processes | Headroom for bursts | Near zero in dynamic/static under load |
| Max children reached (rate) | Pool wanted more workers but hit the ceiling | Incrementing during normal traffic |
| Per-worker RSS | Memory leak detection | Monotonic growth between restarts |
| Worker death rate + exit reason | Stability vs healthy recycling | SIGSEGV/SIGBUS = extension bug; SIGKILL = OOM |
Slow log (request_slowlog_timeout) | Which code path is slow and where it blocks | Disabled (0) by default; the biggest monitoring gap |
| OPcache hit rate + memory | Performance baseline | Hit rate below 99% after warmup; oom_restarts above 0 |
| Accepted connections rate | Inbound throughput | Sudden drop while web server is still busy |
# Operational checks
curl -s http://127.0.0.1/fpm-status | grep -E "^(idle processes|max children reached|accepted conn)"
ps -eo pid,rss,cmd | grep '[p]hp-fpm' | grep -v master | sort -nk2 | tail -1 # highest-RSS worker
tail -20 /var/log/php-fpm/*.slow.log 2>/dev/null || echo "slow log not configured"
grep -c "exited on signal" /var/log/php-fpm/error.log # worker deaths (signal 9 = likely OOM)
Two gaps trap teams here. First, request_slowlog_timeout defaults to 0, so you see that workers are busy but never which code made them busy. Enable it (for example 5 seconds) on every production pool. Second, RSS overstates memory because each worker’s RSS includes shared opcache pages; naive RSS * max_children overestimates total memory by 30-50%, which pushes max_children lower than it needs to be. OPcache status is also not on the FPM status page: it comes from opcache_get_status() through a small PHP endpoint.
Level 3: Mature
You collect leading indicators and composite patterns, and you watch the kernel layer the status page cannot see. This is where you catch saturation before users do.
| Signal | What it tells you | Warning sign |
|---|---|---|
| Per-worker request duration | Latency percentiles, bimodal behavior | Multiple workers above 10x the median |
| Worker age / requests served | Hot endpoints, recycling problems | Workers with huge requests served (recycling broken) |
Socket backlog via ss (Recv-Q) | Kernel-level queue depth | Recv-Q climbing on the LISTEN socket |
| Kernel ListenOverflows / ListenDrops | Connections dropped at the kernel level | Counters rising while FPM shows listen queue = 0 |
| Composite patterns | Exhaustion, slow drain, leak trend, crash loop | active = max_children AND listen queue > 0 for more than 60s |
| Per-pool monitoring | Each pool is independent | Aggregate hides one saturated pool behind a round-robin LB |
| Web server + DB correlation | End-to-end picture | 502/504 with ping OK; FPM workers stuck waiting on DB |
# Mature checks
ss -lxnpt | grep php # Unix socket LISTEN Recv-Q = kernel backlog depth
nstat -az | grep -iE "ListenOverflows|ListenDrops"
curl -s 'http://127.0.0.1/fpm-status?full' | grep "request duration" # microseconds per worker
The status page listen queue is a point-in-time snapshot. It can read 0 while the kernel is dropping connections between polls. On Unix sockets the status page queue often reports 0 regardless, so poll ss directly.
At this level, composite alerts beat single thresholds. “active at ceiling AND listen queue positive AND slow requests rising” identifies a slow dependency drain. “per-worker RSS climbing AND pm.max_requests unset” identifies a leak. Each metric alone is ambiguous; together they name the pattern.
Level 4: Expert
The deep signals that operators add after their third major incident. These explain why workers misbehave and why capacity math keeps lying to you.
| Signal | What it tells you | Warning sign |
|---|---|---|
| Fork latency | Spawn-to-ready cost in dynamic/ondemand during bursts | Workers fork slower than the backlog fills |
PSS accounting (smaps_rollup) | Accurate shared/private memory for capacity math | RSS-based max_children set 30-50% too low |
request_terminate_timeout kills | Workers that hit the hard timeout (hung, not just slow) | Recurring kills = stuck requests eating slots |
| Phantom workers | Workers marked Running whose client already timed out | nginx timed out first; FPM still computing a dead response |
cgroup oom_kill / memory.events | Container memory kills | cgroup v2 OOM absent from dmesg |
# Expert checks
awk '/^Pss:/ {sum+=$2} END {print sum/1024 " MB PSS"}' /proc/$(pgrep -f 'php-fpm: pool' | head -1)/smaps_rollup
grep -c "execution timed out" /var/log/php-fpm/error.log # request_terminate_timeout kills
grep -E "signal 9|SIGKILL" /var/log/php-fpm/error.log # likely OOM (kernel or cgroup)
ss -tnp state time-wait '( sport = :9000 )' | wc -l # TIME_WAIT churn on FPM TCP socket
Beyond the five core signals, expert coverage includes per-endpoint latency from the full status script field, session lock contention (slow-log traces blocking at session_start()), emergency restart events, per-worker file descriptor usage, and security auditing of the status page exposure. Keep the status page internal: the FPM status endpoint has had XSS exposure (CVE-2026-6735, fixed in PHP 8.2.31, 8.3.31, 8.4.21, 8.5.6), and the full mode leaks per-request URIs and script paths regardless.
Two gotchas dominate this level. Phantom workers come from a timeout mismatch: if nginx’s fastcgi_read_timeout fires before FPM’s request_terminate_timeout, nginx returns 504 to the user while the FPM worker keeps computing a response nobody will read. Align timeouts across the whole request path. In containers, FPM has no awareness of cgroup limits. A worker allocates until the cgroup OOM killer strikes, and on cgroup v2 those kills do not appear in dmesg. The trace is in journalctl -u php-fpm.service (or your container runtime logs) reporting that a process was killed by the OOM killer.
How Netdata helps
- Per-second collection of the PHP-FPM status page surfaces active/idle/total processes, listen queue,
max children reached, slow requests, and accepted connection rate at a resolution that catches saturation events 10-second polls miss. - Correlating the FPM pool view with nginx/Apache 502/504 rates distinguishes “FPM down” from “workers saturated” from “workers stuck on I/O.”
- The slow log and worker death rate, read alongside per-worker RSS trends, separate healthy
pm.max_requestsrecycling from a real leak or extension segfault. - OPcache hit rate and memory usage, collected separately from the FPM status page, reveal thrashing that otherwise looks like unexplained CPU and latency across all workers.
- cgroup memory and OOM metrics catch container kills that never reach
dmesg, which matters once PHP-FPM runs under aMemoryMax.
Related guides
- PHP-FPM monitoring checklist: the signals every production pool needs
- How PHP-FPM actually works in production: a mental model for operators
- PHP-FPM worker exhaustion: all workers busy and requests piling into the backlog
- PHP-FPM “server reached pm.max_children setting (N), consider raising it”
- PHP-FPM active processes near max_children: reading pool utilization
- PHP-FPM idle processes at zero: no burst headroom left
- PHP-FPM listen queue growing: the earliest signal of saturation
- PHP-FPM sizing pm.max_children: by memory, not by CPU cores
- PHP-FPM slow request cascade: one slow dependency drains the whole pool
- PHP-FPM 504 Gateway Timeout: requests accepted but never finishing in time
- PHP-FPM slow log: turning on request_slowlog_timeout to see what is slow
- PHP-FPM request duration climbing: spotting stuck and outlier workers






