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 --> B

Common causes

CauseWhat it looks likeFirst thing to check
PHP extension bug triggered by specific inputDeath log shows SIGSEGV on one URI; core dump points into the extensionrequest uri in ?full status, then coredumpctl or the core file
Corrupted opcache shared memoryWorkers crash immediately after starting; opcache hit rate erraticopcache_get_status() for oom_restarts, then reload FPM to clear SHM
PHP version bug after upgradeCrashes began within hours of a PHP or extension upgradeRecent package changes in the package manager log
OOM kill misreported as a crashdmesg shows OOM events; signal in death log is 11 but memory is the causedmesg for OOM lines, per-worker RSS trend
Fork-unsafe library on macOSWorkers crash on macOS in gettext or libintl pathsHost 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

  1. Confirm the loop is happening. You want at least two of: total processes oscillating rapidly between two polls, an exited on signal rate above a few per second, and accepted conn rate dropping while the web server request rate stays constant.
  2. 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.
  3. Pull the per-worker request URI. The crash is usually tied to one request pattern. With ?full you can see request uri for 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.
  4. Check the kernel log. dmesg often has the segfault address and the faulting module, which is more useful than the FPM log line. If you see Out of memory: Killed process lines for php-fpm workers, treat the incident as OOM-driven rather than a true segfault, and pivot to memory diagnosis.
  5. Look at opcache health. Corrupted opcache shared memory is a frequent cause of immediate post-spawn segfaults. Query opcache_get_status() for oom_restarts and wasted_memory. A non-zero oom_restarts counter combined with crashes early in a worker’s life points to SHM corruption.
  6. 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.
  7. Pull a core dump if you can. If rlimit_core is set to unlimited (or any non-zero value) and the kernel core pattern points somewhere useful, the worker will dump core on segfault. coredumpctl list on systemd systems plus gdb on the core file gives you the exact faulting stack frame.

Metrics and signals to monitor

SignalWhy it mattersWarning sign
Worker exit rate (exited on signal in error log)Direct evidence of crashesMultiple per second sustained for more than a minute
Total process countValidates the pool is at its configured sizeWild oscillation, especially below the configured minimum
Accepted conn rateInbound throughputDrops while web server request rate holds steady
Active processesConcurrency under loadStays low despite traffic (workers are dying, not serving)
Per-worker request URI from ?fullPinpoints the crashing code pathOne URI dominates running workers
Emergency restart log linesMaster-triggered full restartsfailed processes threshold repeats
Opcache oom_restartsSHM corruption eventsNon-zero and climbing
System CPUCost of constant forkingSpikes with no application work being done
Web server 502 rateUser-visible impactBursts that align with crash bursts
Kernel OOM events in dmesgRules OOM in or outOut 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 faulty opcache.file_cache, or a kernel SHM issue can all cause it. Increasing opcache.memory_consumption and disabling opcache.file_cache are 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_requests to 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_threshold and emergency_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_output with care. Older PHP versions had a bug where catch_workers_output = yes combined 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, and idle processes catches 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.