nginx logs this error at [error] level when the FastCGI connection to PHP-FPM closes before response headers arrive. The client gets a 502 Bad Gateway. Unlike connect() failed (111: Connection refused) (no worker available) or upstream timed out (110: Connection timed out) (worker too slow), this error means the connection was established, the worker began executing PHP, and the worker vanished before delivering a response.
The worker process died mid-request. The PHP-FPM master reaps the dead child via SIGCHLD and forks a replacement, but the in-flight request is lost. nginx sees the socket close while still waiting on headers and logs “upstream prematurely closed connection while reading response header from upstream”.
Four root-cause families: worker segfault (SIGSEGV or SIGBUS, usually an extension bug or memory corruption), OOM kill (SIGKILL from the kernel or cgroup), request_terminate_timeout firing (PHP-FPM itself killed the worker for exceeding the per-request hard limit), or a PHP fatal where error output exceeded nginx’s FastCGI buffer or the worker aborted before flushing headers. Diagnosis is timestamp correlation across the nginx error log, the FPM error log, dmesg, and the application error log.
What this means
The failure is per-request. Only the request running in the dead worker is affected. A single 502 in the log is usually a one-off segfault on an unusual input. Sustained 502 rates imply a repeating trigger: a code path that segfaults on every call, memory pressure that kills workers as soon as they grow, or a request_terminate_timeout set lower than your slowest legitimate endpoints.
Distinguish this from capacity failures. Worker exhaustion and listen-queue overflow produce connect() failed or no live upstreams, not “upstream prematurely closed”. If you treat a crash-driven 502 as a capacity problem and raise pm.max_children, you will not fix the crashes; on a memory-constrained host you will accelerate OOM kills.
pm.max_requests recycling is not a cause of this error. Workers that hit pm.max_requests finish their current request and deliver the response before self-terminating, then the master spawns a replacement. There is no mid-request interruption.
flowchart TD
A[nginx accepts request] --> B[FastCGI conn to FPM worker]
B --> C[Worker starts executing PHP]
C --> D{Worker dies mid-request}
D -- SIGSEGV / SIGBUS --> E[Extension bug or memory corruption]
D -- SIGKILL --> F[OOM killer or cgroup limit]
D -- request_terminate_timeout --> G[Per-request hard timeout fired]
D -- PHP fatal / abort --> H[Fatal error, headers never flushed]
E --> I[Socket closes before headers]
F --> I
G --> I
H --> I
I --> J[nginx logs "upstream prematurely closed"]
J --> K[Client receives 502]Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Worker segfault (SIGSEGV/SIGBUS) | FPM log: child N exited on signal 11 or signal 7. Same URI repeats across deaths. | request uri in full status page; recent extension or PHP upgrade. |
| OOM kill (SIGKILL) | FPM log: child N exited on signal 9. dmesg shows Out of memory: Kill process. | Per-worker RSS, system/cgroup memory, pm.max_requests. |
request_terminate_timeout fired | FPM log: execution timed out ... terminating. Hits the same long-running endpoints. | Compare timeout value against slowest endpoint durations and request_slowlog_timeout. |
| PHP fatal / oversized error output | Application log shows fatal at same timestamp. Often with XDebug or verbose stack traces in production. | nginx fastcgi_buffer_size and PHP log_limit; production error_reporting. |
| Extension abort (SIGABRT/SIGBUS) | FPM log: exited on signal 6 or signal 7. Correlates with a specific PHP version bump. | Recent extension upgrade; rlimit_core and process.dumpable for core dumps. |
Quick checks
All read-only. Scope to the incident window.
# nginx errors around the incident timestamp (adjust path to your distro)
grep "upstream prematurely closed" /var/log/nginx/error.log | tail -20
# Distinguish from related nginx upstream errors
grep -E "connect\(\) failed|no live upstreams|upstream timed out" /var/log/nginx/error.log | tail -20
# PHP-FPM worker deaths (adjust log path - common: /var/log/php-fpm/error.log or www-error.log)
grep "exited on signal" /var/log/php-fpm/error.log | tail -30
# Segfaults and bus errors specifically
grep -E "signal 11|signal 7|SIGSEGV|SIGBUS" /var/log/php-fpm/error.log | tail -30
# request_terminate_timeout kills
grep "execution timed out" /var/log/php-fpm/error.log | tail -30
# OOM kills from the kernel (signal 9 deaths usually originate here)
dmesg -T | grep -iE "out of memory|oom-kill|killed process" | tail -30
journalctl -k --since "1 hour ago" | grep -i oom
# Current pool state (path depends on pm.status_path config)
curl -s http://127.0.0.1/fpm-status
# Effective per-pool timeout settings
php-fpm -tt 2>&1 | grep -E "request_terminate_timeout|request_slowlog_timeout|pm.max_requests|emergency_restart"
If request_terminate_timeout is 0 in the output, that family is ruled out. The deaths are coming from crashes or OOM.
How to diagnose it
Anchor on a single nginx error timestamp. Pick one 502 from the log and note the second-precise time. Every check below is filtered to that window.
Pull FPM child-exit entries for that window. A signal-11, signal-7, signal-9, or
execution timed outentry within a few seconds of the nginx error confirms a worker died on that request. The exit signal tells you the family: 11 or 7 is a crash, 9 is OOM, the timeout message isrequest_terminate_timeout.Cross-reference
dmesgfor OOM kills. A signal-9 death with a matchingOut of memory: Killed processline indmesg(ormemory.events.oom_killin the cgroup on containers) confirms memory pressure, not an extension bug. The absence of an OOM entry with a signal-9 death should prompt a closer look at systemdKillMode/OOMPolicyand orchestrator eviction.Pull the per-worker
request urifrom full status during recurrence. If crashes cluster on a single endpoint, the trigger is in that code path or its inputs. Usecurl -s http://127.0.0.1/fpm-status?fulland watch which URI is in theRunningstate immediately before each death.Check the application error log at the same timestamp. A PHP fatal (
Fatal error: ...,Allowed memory size of ... exhausted) recorded at the moment of the 502 means the worker aborted on an application condition. If the fatal produced a large stack trace, suspect nginxfastcgi_buffer_sizebeing too small to hold the headers plus the error output.Look for repeating crash loops. Many
exited on signalentries in a tight window, combined with the master loggingfailed processes threshold ... reached, initiating reload(whenemergency_restart_thresholdis configured), means a crash loop, not a one-off. Roll back the most recent deployment or extension upgrade.Verify timeout coherence across the request path. nginx
fastcgi_read_timeout, PHP-FPMrequest_terminate_timeout, and PHPmax_execution_timeshould be coherent. Ifrequest_terminate_timeoutis set lower than your slowest legitimate endpoint, it will kill workers on healthy slow requests and surface here.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
| Worker exit rate by signal | Separates crashes (11/7) from OOM (9) from timeout kills from normal recycling. | Sustained non-zero rate of signal 11/7/9 outside reload windows. |
| Per-worker RSS | Leading indicator for OOM-driven signal-9 deaths. | Monotonic growth with pm.max_requests = 0. |
total processes vs configured pm | Crash loops leave the pool below max_children while the master plays catch-up. | Count dropping below expected mid-traffic in static mode. |
request_terminate_timeout kill count | Each kill is a 502 to the client that requested that endpoint. | Counter climbing on specific slow endpoints. |
| System/cgroup memory pressure | OOM kills happen at the kernel level, invisible to FPM logs except as a signal-9 child exit. | Available memory trending toward zero; cgroup memory.events.oom_kill incrementing. |
| Emergency restart events | When configured, repeated triggers indicate a systemic crash source. | Two or more “failed processes threshold” entries within 15 minutes. |
| Slow log entries | Identifies which endpoints approach the request_terminate_timeout boundary. | Slow-log stack traces matching the killed request URIs. |
| nginx 502 rate | The user-facing symptom; the earliest external signal. | Any sustained non-zero 502 rate on PHP routes. |
Fixes
Worker segfaults (signal 11 or 7)
The crash is almost always in a C extension or the PHP runtime itself, not in PHP userland. Identify the trigger URI from full status, then narrow the cause.
- If a recent PHP or extension upgrade preceded the crashes, roll back and re-test before bisecting.
- Enable core dumps for definitive stack traces: set
rlimit_core = unlimitedin the pool config and ensureprocess.dumpable = yes(needed when the worker runs under a different user/group than the master). Then inspect withcoredumpctlor your configured core path. - If the crash is reproducible on a single endpoint, block or rate-limit that route at the nginx layer while the extension is fixed.
- Do not paper over a segfault by raising
emergency_restart_threshold. That buys availability at the cost of masking the bug.
OOM kills (signal 9)
The worker was killed by the kernel or cgroup because it (or the pool in aggregate) exceeded the memory budget.
- Verify the binding constraint. On bare metal, compare
avg_worker_RSS * pm.max_children + OS_overheadagainst total RAM. In containers, check the cgroupmemory.maxagainstmemory.currentandmemory.events.oom_kill. - Set
pm.max_requeststo a finite value (500 to 1000 is a common starting range) to bound per-worker growth. With the default of 0, leaks accumulate indefinitely. - Use PSS rather than RSS for capacity math. Shared opcache pages inflate RSS by 30 to 50 percent. Pull PSS from
/proc/<pid>/smaps_rolluporsmem. - If a specific request path spikes memory (large result sets, image processing, unbounded caches), cap it in application code or raise
memory_limitonly for that pool with eyes on the RSS impact.
request_terminate_timeout firing
PHP-FPM killed the worker because the request exceeded the configured hard limit. This is intentional behavior, but it produces this error.
- Confirm the timeout value is intentional. Default in upstream PHP is
0(disabled). Some distro or site packages ship a non-zero value. Check the effective value withphp-fpm -ttrather than assuming. - Compare the timeout against the slowest legitimate endpoint. If
request_terminate_timeoutis 30 seconds and your report endpoint legitimately takes 45, either raise the limit for that pool or move the workload to a queue. - If long-running shutdown handlers or
fastcgi_finish_request()work is surviving the timeout, enablerequest_terminate_timeout_track_finishedso the limit covers the full request lifecycle. - Coordinate with nginx
fastcgi_read_timeout. If nginx times out first, you get a 504 and a phantom worker still running an unwanted response; if FPM kills first, you get this 502.
PHP fatal or oversized error output
The worker hit a fatal error and either aborted or produced headers plus error output that exceeded nginx’s FastCGI buffer.
- Check the application error log at the incident timestamp for the actual fatal. A missing fatal entry suggests the worker died before the error handler ran, pointing back to a crash.
- If fatals only reproduce with XDebug or high
error_reportingin production, disable XDebug in production and lowererror_reportingto production-appropriate levels. Large stack traces are the common cause of header overflow. - Increase nginx
fastcgi_buffer_sizeandfastcgi_buffersif legitimate error output is filling them. The defaults are platform-dependent (commonly 4k or 8k). - Raise PHP-FPM
log_limit(PHP 7.3+, default 1024) so stack traces in the FPM log are not truncated mid-investigation.
Prevention
- Set
pm.max_requeststo a finite value. Default 0 means unbounded growth and is the enabler for most OOM-driven premature closes. - Set
request_terminate_timeoutdeliberately. Either disable it (0) and rely onmax_execution_timeplus application timeouts, or pick a value above your slowest legitimate endpoint. Documenting the choice prevents silent regressions. - Configure
emergency_restart_thresholdandemergency_restart_interval. Defaults are 0 (disabled). A reasonable starting point likeemergency_restart_threshold = 10andemergency_restart_interval = 60gives the master a circuit breaker for crash loops instead of forking into oblivion. - Enable
request_slowlog_timeout. The slow log is the only signal that tells you which code path approaches therequest_terminate_timeoutboundary before users see 502s. - Track per-worker RSS over time. Catch the leak before the OOM killer does. PSS is more accurate than RSS for the capacity math.
- Keep PHP and extensions current on a supported branch. Segfault-causing bugs in older extension versions are a frequent root cause after a PHP upgrade exposes them.
- Verify timeout coherence on every nginx or FPM config change.
fastcgi_read_timeout,request_terminate_timeout, andmax_execution_timemust tell a consistent story across the request path. - Monitor kernel OOM events and cgroup
memory.events.oom_kill. FPM only sees these as a signal-9 child exit after the fact.
How Netdata helps
- The PHP-FPM collector surfaces
active processes,idle processes,total processes,listen queue, andmax children reachedper second, so a crash-loop drop intotal processesis visible in the same window as the nginx 502 spike rather than minutes later. - Per-process RSS collection (via the
appsorcgroupplugins, depending on deployment) lets you see the monotonic per-worker memory climb that precedes signal-9 OOM deaths, alongside system and cgroup memory pressure. - The nginx collector surfaces upstream error counts and 4xx/5xx response rates, so the 502 pattern is correlatable with FPM worker exits in a single timeline.
- ML anomaly detection on
total processes, accepted connection rate, and worker exit rate flags the deviation from baseline that precedes a crash-driven 502 wave, which is useful when the underlying segfault is intermittent. - Kernel OOM kill counters and cgroup
memory.events.oom_killare collected directly, so a signal-9 child exit can be matched to a kernel-level kill event without switching tools.
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
- PHP-FPM “child N exited on signal 11 (SIGSEGV)”: worker segfaults
- PHP-FPM crash loop and fork storm: workers dying faster than they serve
- PHP-FPM dynamic mode scaling lag: why the pool cannot keep up with bursts
- PHP-FPM emergency restart: “failed processes threshold reached, initiating reload”
- PHP-FPM graceful reload: the brief no-worker window on SIGUSR2
- 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”






