LEMP Field Notes
Nginx & PHP

Nginx 502 Bad Gateway with PHP-FPM: 6 Causes and Fixes

Nginx 502 Bad Gateway with PHP-FPM: 6 Causes and Fixes
In briefA 502 Bad Gateway on Nginx with PHP-FPM means Nginx received the request but got no valid response from PHP-FPM. The usual causes: PHP-FPM is stopped, the fastcgi_pass path does not match PHP-FPM's listen socket, a socket/TCP mismatch, socket permission errors, an exhausted pm.max_children worker pool, or crashed workers. Check /var/log/nginx/error.log first — the exact error message identifies which cause applies, then restart PHP-FPM or align the two configs.

What a 502 Bad Gateway Means on a LEMP Stack

A 502 Bad Gateway error means Nginx accepted the request but got no valid response from the upstream service it proxied to — on a LEMP stack, that upstream is almost always PHP-FPM. Nginx itself is running fine; the handoff to PHP failed. Either PHP-FPM is not running, Nginx is pointing at the wrong socket or port, permissions block the connection, or PHP-FPM is running but too overloaded or broken to answer.

That framing matters because it tells you where to look. The problem is rarely in your HTML, your PHP code, or your DNS. It lives somewhere on the short path between Nginx and PHP-FPM: the service itself, the address Nginx dials in fastcgi_pass, the socket or port that connects them, or the pool's ability to actually answer once the connection is made.

This guide walks through the six causes that account for nearly every 502 on an Nginx + PHP-FPM server, in the order you should check them. Each fix includes the exact commands and the log lines that confirm you found the right culprit. If you are still setting up your stack, our LEMP install guide for Ubuntu 24.04 shows the working baseline configuration these fixes assume.

First: Read the Nginx Error Log

Before changing anything, look at the actual error. The Nginx error log tells you why the gateway failed, and the message maps almost directly to one of the six causes below.

sudo tail -20 /var/log/nginx/error.log

The three messages you will most likely see:

With the log line in hand, jump to the matching cause. If the log shows something else — timeouts, "upstream prematurely closed connection" — Causes 5 and 6 cover those.

Cause 1: PHP-FPM Is Not Running

The most common cause of a 502 is simply that the PHP-FPM service is stopped, crashed, or was never started after installation.

Check its status. On Ubuntu 24.04 the default package is PHP 8.3, so the unit is named php8.3-fpm; adjust the version number to match your installed PHP:

sudo systemctl status php8.3-fpm

If the output shows inactive (dead) or failed, start it and enable it so it survives reboots:

sudo systemctl start php8.3-fpm
sudo systemctl enable php8.3-fpm

If the service refuses to start, the reason is usually a syntax error in a pool file. PHP-FPM ships its own config tester, just like nginx -t:

sudo php-fpm8.3 -t

Fix whatever it reports, then start the service again. Reload your site — if the 502 is gone, you are done. If PHP-FPM keeps dying after starting cleanly, skip ahead to Cause 6.

Cause 2: Nginx Points at the Wrong Socket Path

If PHP-FPM is running but the error log says No such file or directory, Nginx and PHP-FPM disagree about where the socket lives. This is extremely common after a PHP upgrade: you install PHP 8.3, but your server block still says php8.1-fpm.sock, and that file no longer exists.

Find out where PHP-FPM is actually listening. The listen directive in the pool config is the source of truth:

grep -r "^listen" /etc/php/8.3/fpm/pool.d/

On Debian and Ubuntu the default is a Unix socket:

listen = /run/php/php8.3-fpm.sock

Now check what Nginx thinks:

grep -r "fastcgi_pass" /etc/nginx/sites-enabled/

The two values must match exactly. A correct PHP location block in your Nginx server block looks like this:

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

If they differ, edit the server block so fastcgi_pass matches the pool's listen value, then test and reload:

sudo nginx -t
sudo systemctl reload nginx

You can also confirm the socket file really exists on disk:

ls -l /run/php/

A healthy socket shows up as a file of type s (socket) owned by www-data on Ubuntu. No file means PHP-FPM is not running or is listening on TCP instead — which brings us to the next cause.

Cause 3: Connection Refused on a TCP Port

The error connect() failed (111: Connection refused) while connecting to upstream means Nginx tried to open a TCP connection — typically to 127.0.0.1:9000 — and the kernel refused it because nothing is listening on that port.

This happens when Nginx is configured for TCP (fastcgi_pass 127.0.0.1:9000;) while PHP-FPM is listening on a Unix socket — or is not running at all. Docker-derived tutorials often show the TCP form, which then fails on a stock Ubuntu install where the package defaults to a socket. Note that the mirror-image mistake — Nginx pointed at a socket while PHP-FPM listens on TCP — produces (2: No such file or directory) instead, because there is no socket file to open. Only the TCP direction can return "connection refused."

Verify what is actually listening:

sudo ss -tlnp | grep 9000

No output means nothing owns port 9000. You have two valid fixes — pick one and make both sides agree.

Option A — switch Nginx to the socket (recommended on a single server):

fastcgi_pass unix:/run/php/php8.3-fpm.sock;

Option B — switch PHP-FPM to TCP. Edit /etc/php/8.3/fpm/pool.d/www.conf and change the listen line:

listen = 127.0.0.1:9000

Then restart PHP-FPM and reload Nginx:

sudo systemctl restart php8.3-fpm
sudo nginx -t && sudo systemctl reload nginx

Unix Socket vs TCP: Which Should You Use?

Unix socket TCP (127.0.0.1:9000)
Overhead Lower — no TCP/IP stack Slightly higher
Works across hosts/containers No — same machine only Yes
Failure mode File permissions, missing file Port conflicts, firewall rules
Debian/Ubuntu package default Yes No

The practical rule: when Nginx and PHP-FPM run on the same machine, use the Unix socket — it is the distro default and avoids port management entirely. Use TCP only when PHP-FPM runs on a different host or in a separate container, where a socket file cannot be shared.

Cause 4: Socket Permission Denied

A (13: Permission denied) error means the socket exists and PHP-FPM is running, but the user Nginx's worker processes run as cannot connect to it.

On Ubuntu and Debian, both Nginx workers and the default PHP-FPM pool run as www-data, so the stock configuration works out of the box. The error appears when one side has been changed — a custom pool running as a different user, or Nginx workers switched away from www-data.

Check the socket's ownership settings in the pool file:

grep -E "^;?\s*listen\.(owner|group|mode)" /etc/php/8.3/fpm/pool.d/www.conf

The ;? in that pattern is deliberate: on stock Debian and Ubuntu, listen.mode ships commented out as ;listen.mode = 0660. That is not a missing setting — a commented line simply leaves PHP-FPM's built-in default of 0660 in force. An anchored ^listen\. grep would hide it and send you looking for a problem that is not there.

The values you want in effect:

listen.owner = www-data
listen.group = www-data
listen.mode = 0660

And confirm which user Nginx runs as:

grep -E "^user" /etc/nginx/nginx.conf

The fix: make listen.owner and listen.group match the Nginx worker user (or put that user in the socket's group), keep listen.mode = 0660, then restart PHP-FPM. Avoid the tempting shortcut of listen.mode = 0666 — a world-writable socket lets any local user send requests directly to your PHP pool, which is a real risk on shared servers.

Cause 5: PHP-FPM Is Overloaded — All Workers Busy

If the 502s are intermittent — the site works, then throws 502s under traffic, then recovers — PHP-FPM is likely running out of worker processes. Every incoming PHP request needs a free worker; when all of them are busy, new connections pile up in the pool's listen backlog instead of being served.

Be aware of the failure order here, because it decides which error you see. While the backlog still has room, connections just wait, and the usual symptom is a 504 Gateway Timeout — Nginx gave up waiting after fastcgi_read_timeout. The 502 appears at the next stage: once the backlog itself is full, the kernel stops accepting new connections and Nginx gets a refused or reset connection instead of a slow one. So a saturated pool typically produces a burst of 504s first, with 502s arriving as the load gets worse. Seeing either one under traffic points at this section.

The telltale line lives in the PHP-FPM log, not the Nginx log:

sudo grep "max_children" /var/log/php8.3-fpm.log

If you see server reached pm.max_children setting, the pool is saturated. The setting lives in /etc/php/8.3/fpm/pool.d/www.conf:

pm = dynamic
pm.max_children = 5

The default of 5 is deliberately conservative and easily overwhelmed on a busy site. To raise it safely, base the number on memory: check the average size of a PHP-FPM process, divide your spare RAM by that, and leave headroom for Nginx and MySQL.

ps --no-headers -o rss -C php-fpm8.3 | awk '{sum+=$1; n++} END {if (n) printf "%.0f MB avg\n", sum/n/1024}'

If each worker averages roughly 60 MB and you can spare about 1 GB for PHP, around 16 workers is a reasonable ceiling — recompute for your own numbers rather than copying anyone's. Set pm.max_children accordingly and restart PHP-FPM.

Slow PHP code makes this worse: a request that takes 30 seconds holds a worker hostage for 30 seconds. If saturation persists after raising the limit, profile what the slow requests are doing (often the database) before adding more workers.

Cause 6: Crashes, Timeouts, and Oversized Headers

The remaining 502s come from PHP-FPM accepting the connection but failing to deliver a usable response.

Worker crashes

upstream prematurely closed connection while reading response header in the Nginx log means the PHP worker died mid-request — commonly a fatal error in a PHP extension or an out-of-memory kill.

The detail that trips people up is how PHP-FPM records a dead worker. It does not write the words "segfault" or "killed" to its error log. It writes the signal that killed the child:

WARNING: [pool www] child 1234 exited on signal 11 (SIGSEGV - core dumped) after 3.472 seconds from start
WARNING: [pool www] child 1235 exited on signal 9 (SIGKILL) after 61.203 seconds from start

Signal 11 is a crash; signal 9 is almost always the kernel's OOM killer (or something else sending SIGKILL). So grep the FPM log for the signal vocabulary it actually uses:

sudo grep -iE "SIGSEGV|SIGKILL|SIGBUS|exited on signal" /var/log/php8.3-fpm.log

The literal string segfault belongs to the kernel ring buffer, not the FPM log, so check dmesg separately for both crashes and OOM kills:

sudo dmesg -T | grep -iE "segfault|out of memory|oom-killer"

Running only one of these will mislead you: the FPM log tells you a worker died and which signal did it, while dmesg tells you what the kernel saw. If the kernel is OOM-killing workers, lower pm.max_children or PHP's memory_limit, or add RAM. If a specific extension segfaults, disabling it confirms the diagnosis.

Timeouts

Nginx waits a limited time for the FastCGI response — the fastcgi_read_timeout directive, 60 seconds by default. A long-running request that exceeds it is cut off. Note that a pure timeout usually returns 504 Gateway Timeout, not 502 — but PHP-FPM's own request_terminate_timeout, if set, kills the worker outright and produces a 502. Align the two: PHP's limit should be at or below Nginx's, and both should be as low as your slowest legitimate request allows.

FastCGI buffers

Rarely, a 502 with upstream sent too big header in the log means a PHP response's headers overflowed Nginx's FastCGI buffers — large cookie payloads or frameworks emitting big headers can trigger it. Raising the buffers in the PHP location block resolves it:

fastcgi_buffer_size 32k;
fastcgi_buffers 16 32k;

Reload Nginx after the change.

A 5-Minute 502 Checklist

When a 502 hits in production, run this sequence top to bottom:

  1. Read the log: sudo tail -20 /var/log/nginx/error.log — the error message picks the cause.
  2. Check the service: sudo systemctl status php8.3-fpm — start it if it is down.
  3. Match the paths: compare fastcgi_pass in Nginx against listen in the pool config.
  4. Check permissions: socket owned by the Nginx worker user, mode 0660.
  5. Check saturation: grep the PHP-FPM log for max_children — expect 504s alongside the 502s.
  6. Check crashes: sudo grep -iE "SIGSEGV|SIGKILL|exited on signal" /var/log/php8.3-fpm.log, then sudo dmesg -T | grep -iE "segfault|out of memory".
  7. Test before reload: sudo nginx -t && sudo systemctl reload nginx after any config edit.

One last pointer: a misconfigured HTTPS setup is not a cause of the FastCGI 502s covered here — adding TLS in front of PHP-FPM does not change the Nginx-to-PHP handoff at all. (502s from TLS are possible in a different setup: an Nginx proxy_pass to an https:// upstream can return 502 when the upstream certificate fails verification or the two sides disagree on protocol. That is a reverse-proxy problem, not a PHP-FPM one.) If you fixed your gateway and are now moving on to hardening, securing Nginx with Let's Encrypt is the natural next step — and the FastCGI configuration you just verified carries over unchanged to the TLS server block.

FAQ

What causes a 502 Bad Gateway error with Nginx and PHP-FPM?

A 502 means Nginx could not get a valid response from PHP-FPM. The six common causes are: the PHP-FPM service is stopped or crashed, Nginx's fastcgi_pass points at the wrong socket path (common after PHP upgrades), a Unix-socket vs TCP mismatch between the two configs, socket permission errors, all PHP-FPM workers busy because pm.max_children is too low, or workers dying mid-request from fatal errors or out-of-memory kills.

How do I check if PHP-FPM is running?

Run sudo systemctl status php8.3-fpm, adjusting the version number to your installed PHP. If it shows inactive or failed, start it with sudo systemctl start php8.3-fpm and enable it at boot with systemctl enable. If it refuses to start, test the configuration with sudo php-fpm8.3 -t, which reports syntax errors in pool files the same way nginx -t does for Nginx.

What does 'connect() failed (111: Connection refused) while connecting to upstream' mean?

It means Nginx tried to open a TCP connection to the upstream — typically 127.0.0.1:9000 — and nothing was listening on that port. Usually Nginx is configured for TCP while PHP-FPM listens on a Unix socket, or PHP-FPM is down. Verify with sudo ss -tlnp | grep 9000, then make both sides agree: either point fastcgi_pass at the Unix socket or set PHP-FPM's listen directive to 127.0.0.1:9000.

Should PHP-FPM use a Unix socket or TCP port?

Use a Unix socket when Nginx and PHP-FPM run on the same machine — it has lower overhead, avoids port management, and is the Debian/Ubuntu package default (/run/php/php8.3-fpm.sock). Use TCP (127.0.0.1:9000) only when PHP-FPM runs on a different host or in a separate container, where a socket file cannot be shared. Whichever you choose, the fastcgi_pass and listen directives must match exactly.

How do I fix a PHP-FPM socket permission denied error?

A (13: Permission denied) error means the Nginx worker user cannot open the socket. In the pool file (/etc/php/8.3/fpm/pool.d/www.conf), set listen.owner and listen.group to the Nginx worker user — www-data on Ubuntu — with listen.mode = 0660, then restart PHP-FPM. Avoid 0666: a world-writable socket lets any local user send requests straight to your PHP pool.

Why do I get 502 errors only under heavy traffic?

Intermittent 502s under load usually mean the PHP-FPM worker pool is saturated. Check the PHP-FPM log for 'server reached pm.max_children setting'. Expect 504 Gateway Timeout errors first — requests wait in the pool's listen backlog until Nginx gives up — with 502s appearing once the backlog fills and connections are refused outright. The Ubuntu default of 5 workers is conservative; raise pm.max_children based on memory, dividing the RAM you can spare for PHP by the average worker size and leaving headroom for Nginx and MySQL. Also profile slow requests, since each one holds a worker for its full duration.