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

CauseWhat it looks likeFirst 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 firedFPM 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 outputApplication 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

  1. 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.

  2. Pull FPM child-exit entries for that window. A signal-11, signal-7, signal-9, or execution timed out entry 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 is request_terminate_timeout.

  3. Cross-reference dmesg for OOM kills. A signal-9 death with a matching Out of memory: Killed process line in dmesg (or memory.events.oom_kill in 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 systemd KillMode/OOMPolicy and orchestrator eviction.

  4. Pull the per-worker request uri from full status during recurrence. If crashes cluster on a single endpoint, the trigger is in that code path or its inputs. Use curl -s http://127.0.0.1/fpm-status?full and watch which URI is in the Running state immediately before each death.

  5. 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 nginx fastcgi_buffer_size being too small to hold the headers plus the error output.

  6. Look for repeating crash loops. Many exited on signal entries in a tight window, combined with the master logging failed processes threshold ... reached, initiating reload (when emergency_restart_threshold is configured), means a crash loop, not a one-off. Roll back the most recent deployment or extension upgrade.

  7. Verify timeout coherence across the request path. nginx fastcgi_read_timeout, PHP-FPM request_terminate_timeout, and PHP max_execution_time should be coherent. If request_terminate_timeout is set lower than your slowest legitimate endpoint, it will kill workers on healthy slow requests and surface here.

Metrics and signals to monitor

SignalWhy it mattersWarning sign
Worker exit rate by signalSeparates 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 RSSLeading indicator for OOM-driven signal-9 deaths.Monotonic growth with pm.max_requests = 0.
total processes vs configured pmCrash 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 countEach kill is a 502 to the client that requested that endpoint.Counter climbing on specific slow endpoints.
System/cgroup memory pressureOOM 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 eventsWhen configured, repeated triggers indicate a systemic crash source.Two or more “failed processes threshold” entries within 15 minutes.
Slow log entriesIdentifies which endpoints approach the request_terminate_timeout boundary.Slow-log stack traces matching the killed request URIs.
nginx 502 rateThe 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 = unlimited in the pool config and ensure process.dumpable = yes (needed when the worker runs under a different user/group than the master). Then inspect with coredumpctl or 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_overhead against total RAM. In containers, check the cgroup memory.max against memory.current and memory.events.oom_kill.
  • Set pm.max_requests to 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_rollup or smem.
  • If a specific request path spikes memory (large result sets, image processing, unbounded caches), cap it in application code or raise memory_limit only 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 with php-fpm -tt rather than assuming.
  • Compare the timeout against the slowest legitimate endpoint. If request_terminate_timeout is 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, enable request_terminate_timeout_track_finished so 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_reporting in production, disable XDebug in production and lower error_reporting to production-appropriate levels. Large stack traces are the common cause of header overflow.
  • Increase nginx fastcgi_buffer_size and fastcgi_buffers if 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_requests to a finite value. Default 0 means unbounded growth and is the enabler for most OOM-driven premature closes.
  • Set request_terminate_timeout deliberately. Either disable it (0) and rely on max_execution_time plus application timeouts, or pick a value above your slowest legitimate endpoint. Documenting the choice prevents silent regressions.
  • Configure emergency_restart_threshold and emergency_restart_interval. Defaults are 0 (disabled). A reasonable starting point like emergency_restart_threshold = 10 and emergency_restart_interval = 60 gives 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 the request_terminate_timeout boundary 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, and max_execution_time must 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, and max children reached per second, so a crash-loop drop in total processes is visible in the same window as the nginx 502 spike rather than minutes later.
  • Per-process RSS collection (via the apps or cgroup plugins, 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_kill are collected directly, so a signal-9 child exit can be matched to a kernel-level kill event without switching tools.