Workers start, accept a request, hit a crash-inducing code path, die with SIGSEGV or SIGBUS, and get respawned by the master. The replacement picks up another request from the same traffic pattern, hits the same code, and dies again. The pool spends more time forking and dying than serving. Each fork costs the kernel time to copy page tables and costs PHP time to initialize the runtime. Users see intermittent 502s while total processes oscillates and the accepted conn rate collapses.
Unlike worker exhaustion (workers are busy but alive) or a memory leak spiral (workers grow over hours), here workers die in seconds or milliseconds. The crash is almost always tied to a specific request pattern. The fix is to identify the crashing URI from the full status page, block it at the web server, and then repair the extension or code that segfaults.
This article assumes you understand the master-worker model and that each worker handles exactly one request at a time. For that background, see how PHP-FPM actually works in production.
What this means
A PHP-FPM worker exits abnormally when the PHP runtime or a loaded extension crashes. The master process reaps the dead child via SIGCHLD and forks a replacement to keep the pool at its configured size. The replacement inherits traffic from the listen socket, hits the same crashing code, and exits again. There is no exponential backoff in the master’s respawn logic. If the request pattern keeps arriving, the pool enters a tight die-respawn loop.
At a high crash rate, system CPU climbs from constant fork() calls, opcache effectiveness drops because new workers must warm up, and effective throughput collapses. The master is still alive and the ping endpoint may still respond, so a basic liveness check will not flag the problem. You need process counts, exit signals, and per-worker request URIs.
If emergency_restart_threshold and emergency_restart_interval are configured, the master will eventually execvp() itself and restart the entire pool when N children die with SIGSEGV or SIGBUS within the configured interval. Both default to 0, which means the feature is disabled in many deployments. With it disabled, the fork storm runs unbounded. With it enabled, the pool oscillates between crash loop and full restart, producing periodic total outages.
flowchart TD
A[Request with triggering input] --> B[Master assigns idle worker]
B --> C[Worker executes request]
C --> D{Hits crash code path}
D -->|SIGSEGV or SIGBUS| E[Worker dies]
E --> F[Master forks replacement]
F --> G[Fork cost: page tables + PHP init]
G --> H[Replacement picks up next request]
H --> C
D -->|emergency_restart_threshold hit| I[Master execvp restart]
I --> J[Brief total outage]
J --> BCommon causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| PHP extension bug triggered by specific input | Death log shows SIGSEGV on one URI; core dump points into the extension | request uri in ?full status, then coredumpctl or the core file |
| Corrupted opcache shared memory | Workers crash immediately after starting; opcache hit rate erratic | opcache_get_status() for oom_restarts, then reload FPM to clear SHM |
| PHP version bug after upgrade | Crashes began within hours of a PHP or extension upgrade | Recent package changes in the package manager log |
| OOM kill misreported as a crash | dmesg shows OOM events; signal in death log is 11 but memory is the cause | dmesg for OOM lines, per-worker RSS trend |
| Fork-unsafe library on macOS | Workers crash on macOS in gettext or libintl paths | Host platform, LANG and LC_ALL environment |
Quick checks
Read-only and safe on a production host. Run them in order.
# Worker death rate in the FPM error log
grep -c "exited on signal" /var/log/php-fpm/error.log
# Recent worker deaths with signal numbers and PIDs
grep "exited on signal" /var/log/php-fpm/error.log | tail -20
# Kernel-level segfault detail (often richer than the FPM log line)
dmesg | grep php-fpm | tail -20
# Is the master still alive?
pgrep -f "php-fpm: master" > /dev/null && echo "UP" || echo "DOWN"
# Status page snapshot: total, active, idle, accepted conn
curl -s http://127.0.0.1/fpm-status
# Per-worker detail: find the URI that triggers the crash
curl -s 'http://127.0.0.1/fpm-status?full' | grep -E "PID|request URI|state"
# Kernel OOM events (in case signal 11 is really an OOM kill)
dmesg | grep -i "out of memory" | tail -10
# Emergency restart events (only present if threshold is configured)
grep -E "failed processes threshold|exiting, bye-bye|ready to handle connections" /var/log/php-fpm/error.log
# Accepted connection rate: sample twice with a known interval
curl -s http://127.0.0.1/fpm-status | grep "^accepted conn"; sleep 5; curl -s http://127.0.0.1/fpm-status | grep "^accepted conn"
If your socket path or status path differs, adjust accordingly. The status page is a FastCGI resource. If the web server does not proxy the status path, hit the socket directly with cgi-fcgi -bind -connect.
How to diagnose it
- Confirm the loop is happening. You want at least two of:
total processesoscillating rapidly between two polls, anexited on signalrate above a few per second, andaccepted connrate dropping while the web server request rate stays constant. - Capture the signal number. SIGSEGV is signal 11. SIGBUS is signal 7. SIGABRT is signal 6 (assertion failure in an extension). SIGKILL is signal 9 and means the OOM killer or an external actor, not a PHP crash. The signal narrows the cause.
- Pull the per-worker request URI. The crash is usually tied to one request pattern. With
?fullyou can seerequest urifor each running worker. If the same URI appears in most running workers and the death log shows signals near the time those workers started, you have the trigger. - Check the kernel log.
dmesgoften has the segfault address and the faulting module, which is more useful than the FPM log line. If you seeOut of memory: Killed processlines for php-fpm workers, treat the incident as OOM-driven rather than a true segfault, and pivot to memory diagnosis. - Look at opcache health. Corrupted opcache shared memory is a frequent cause of immediate post-spawn segfaults. Query
opcache_get_status()foroom_restartsandwasted_memory. A non-zerooom_restartscounter combined with crashes early in a worker’s life points to SHM corruption. - Cross-check recent changes. Crash loops that start within hours of a PHP upgrade, an extension update, or a deploy are almost always tied to that change. Check the package manager log, the deploy log, and the git history of any extension config.
- Pull a core dump if you can. If
rlimit_coreis set tounlimited(or any non-zero value) and the kernel core pattern points somewhere useful, the worker will dump core on segfault.coredumpctl liston systemd systems plusgdbon the core file gives you the exact faulting stack frame.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
Worker exit rate (exited on signal in error log) | Direct evidence of crashes | Multiple per second sustained for more than a minute |
| Total process count | Validates the pool is at its configured size | Wild oscillation, especially below the configured minimum |
| Accepted conn rate | Inbound throughput | Drops while web server request rate holds steady |
| Active processes | Concurrency under load | Stays low despite traffic (workers are dying, not serving) |
Per-worker request URI from ?full | Pinpoints the crashing code path | One URI dominates running workers |
| Emergency restart log lines | Master-triggered full restarts | failed processes threshold repeats |
Opcache oom_restarts | SHM corruption events | Non-zero and climbing |
| System CPU | Cost of constant forking | Spikes with no application work being done |
| Web server 502 rate | User-visible impact | Bursts that align with crash bursts |
Kernel OOM events in dmesg | Rules OOM in or out | Out of memory: Killed process entries |
Fixes
Stop the loop first, then fix the underlying cause.
Immediate mitigation: block the offending URI
If the per-worker request URI analysis points to a single path, block it at the web server while you fix the code. This is faster and safer than restarting FPM, which only buys you seconds before traffic re-triggers the loop.
# nginx: return 503 for the known-bad path while you fix the code
location = /path/to/crashing/endpoint {
return 503;
}
Reload the web server, then watch the death log rate drop. This is a stopgap, not a fix. Track it as a known degraded state.
Immediate mitigation: reload FPM to clear opcache shared memory
If opcache SHM corruption is the suspected cause, a graceful reload (SIGUSR2 or systemctl reload php-fpm) clears the shared memory segment. New workers start with a fresh opcache.
There is a brief window during reload where no workers are available, because SIGUSR2 drains existing workers before the master re-execs itself. Users may see transient 502s. Schedule this outside peak traffic if possible.
Fix the extension or PHP code
Once the loop is stopped, fix the underlying cause:
- Extension bug. Check the extension changelog for segfault fixes. Disable the extension if the application can run without it. Patch or pin to a known-good version.
- Corrupted opcache SHM. Investigate why corruption occurred. Undersized
opcache.memory_consumption, a faultyopcache.file_cache, or a kernel SHM issue can all cause it. Increasingopcache.memory_consumptionand disablingopcache.file_cacheare common fixes. - PHP version bug. Check the PHP changelog for segfault fixes in the version you run. If you are on a known-buggy release window, upgrade.
Configure emergency_restart_threshold as a circuit breaker
If emergency_restart_threshold and emergency_restart_interval are at their default of 0, the master will never emergency-restart. Setting emergency_restart_threshold = 10 and emergency_restart_interval = 60 (10 SIGSEGV or SIGBUS exits within 60 seconds triggers a full restart) gives you a circuit breaker that clears corrupted SHM and resets workers.
Tradeoffs: emergency restart causes a brief total outage and resets opcache, so the first requests after restart are slower. It only counts SIGSEGV and SIGBUS, not OOM kills (signal 9). It restarts all pools, not just the affected one.
Enable core dumps for next time
Set rlimit_core = unlimited in the pool config and configure the kernel core pattern (/proc/sys/kernel/core_pattern) to write dumps to a known location. Without this, post-mortem debugging of segfaults is mostly guesswork. The cost is disk space during crash events.
Prevention
- Set
pm.max_requeststo a finite value (500-1000). Worker recycling clears per-process state that can accumulate into memory corruption. This is the single most common missing safety net. - Configure
emergency_restart_thresholdandemergency_restart_interval. A crash loop without this circuit breaker runs unbounded. - Keep PHP and extensions on supported versions. Most segfault causes are fixed upstream. Running EOL or security-only versions leaves known crash bugs unpatched.
- Enable
catch_workers_outputwith care. Older PHP versions had a bug wherecatch_workers_output = yescombined with a worker crash could crash the master process itself, turning a worker crash loop into a master crash loop. - Pre-deploy extension testing. A short load test against the endpoints that exercise your PHP extensions catches extension-triggered segfaults before production traffic does.
- Monitor worker exit rate as a primary signal. Do not wait for 502s to surface. The death log is the earliest signal that workers are unstable.
How Netdata helps
- Per-second polling of
total processes,active processes, andidle processescatches the rapid oscillation that characterizes a fork storm. Ten-second polling will miss it. - Accepted connection rate as a derived metric shows the throughput collapse that accompanies the loop.
- Anomaly detection on worker exit patterns flags the death rate deviating from baseline before the loop becomes self-sustaining.
- Correlation across FPM, web server, and kernel metrics in one view lets you confirm in seconds whether the crash aligns with a 502 burst, an opcache event, or an OOM kill in
dmesg. - Per-pool visibility matters here, because a crash in one pool does not affect others, but aggregate metrics hide which pool is failing.
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
- 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
- PHP-FPM memory_limit vs worker RSS: why workers exceed the limit you set
- PHP-FPM monitoring checklist: the signals every production pool needs
- PHP-FPM monitoring maturity model: from survival to expert






