๐ Daftar Isi
- Step 1: Read the Nginx Error Log First โ Stop Guessing
- Step 2: Check PHP-FPM Status and Logs
- Step 3: Check Memory and Hunt for OOM Killer Trails
- Step 4: Verify Socket and Port Match Between Nginx and Upstream
- Step 5: Tune PHP-FPM So It Stops Dying
- 502 Bad Gateway Troubleshooting Table
- Pro Tips and Warnings From the Trenches
- FAQ: Questions I Get Asked All the Time
- Q: Why is my site throwing 502 when the server uptime is high and everything looks fine?
- Q: What's the difference between 502 Bad Gateway and 504 Gateway Timeout?
- Q: After changing PHP-FPM config, do I also need to restart Nginx?
- Q: Why do I get 502 errors every few hours like clockwork?
- Q: How do I fix a 502 on a server running cPanel or Plesk?
- Wrapping Up
Fix Nginx 502 Bad Gateway Upstream: Complete Troubleshooting Guide
Let me take you back to 2019. I was a junior NOC engineer back then โ the kind who broke into a cold sweat every time the phone rang after midnight. And one Saturday night, the kind where you’re supposed to be horizontal on the couch with a snack in hand, my phone went off like a fire alarm. Client on the line, voice cracking: “My website has been throwing 502 errors for over an hour! My customers are freaking out!” I remember that call like it was yesterday.
Here’s the embarrassing part: back then, my entire toolkit was one button โ restart. Restart nginx, restart php-fpm, say a little prayer, and hope. Sometimes it worked. Sometimes it didn’t, and then I was stuck staring at a wall of logs I didn’t really know how to read. Seven years and hundreds of 502s later, I can tell you the truth: 502 errors are way less scary once you learn how to read them. They’re not random chaos. They’re a message, and the message is almost always written in the logs. In this guide, I’ll walk you through the whole thing from scratch โ what’s actually happening under the hood, why it happens, and the exact steps to fix it without losing your mind at 3 AM.
Let’s start with a quick mental model, because guessing without one is how you burn three hours. Nginx sits at the front door of your application like a maรฎtre d’. A visitor walks in, the maรฎtre d’ takes the order, then relays it to the kitchen. If nobody in the kitchen answers โ or the response comes back garbled โ the maรฎtre d’ has no choice but to walk back to the guest and shrug: 502 Bad Gateway. In technical terms, that “kitchen” is the upstream: PHP-FPM, Apache, Node.js, Gunicorn, whatever your backend happens to be. Nginx talks to it through fastcgi_pass, proxy_pass, or an upstream block. For the most common setup in this part of the world โ WordPress and PHP sites on shared and VPS hosting โ that upstream is almost always PHP-FPM.
And here’s the part too many people shrug off: a 502 on a production server isn’t just a “website error.” The impact is very real. An e-commerce site down for an hour can lose real revenue โ I’ve watched it happen. SEO takes a hit because Google doesn’t love serving error pages. Customer trust evaporates, especially if it happens right at checkout. And the part I know best: the phone call, the angry client, the management staring at the NOC team like we broke something on purpose. If you run production infrastructure, a 502 is a business problem and a trust problem, not just a tech one.
So why is the 502 so annoying to track down? Simple: it has a hundred possible causes and none of them are visible from the front door. Maybe the PHP-FPM socket is missing because someone upgraded PHP and forgot to update the Nginx config. Maybe the OOM killer quietly executed your PHP workers at 3 AM. Maybe every single PHP worker is busy and requests are piling up into a mountain of 502s. Random guessing won’t get you anywhere โ trust me, I’ve tried, and it wastes hours. You need a systematic approach, and systematic starts with one word: logs. In my experience, 80% of 502 cases are already solved inside the logs if you take the time to actually read them.
One rule before we dive in, and I can’t stress this enough: don’t panic, and don’t reach for the restart button first. Restart is the last step once you understand the problem, not the first. It’s like the check engine light โ a real mechanic doesn’t tear the engine apart, he reads the codes first. Your logs are those codes. Let’s start reading.
Step 1: Read the Nginx Error Log First โ Stop Guessing
The first step to fix nginx 502 bad gateway errors is to open the Nginx error log. On most distros it lives at /var/log/nginx/error.log. If you’re on a panel like cPanel or Plesk the path may differ a little, but the principle is identical. Check the tail of the file, which has the freshest entries:
tail -n 50 /var/log/nginx/error.log
Quick note: Can’t find error.log at that path? Check for error.log.1 (rotated file) or grep the actual configured path with grep error_log /etc/nginx/nginx.conf. Don’t burn twenty minutes hunting for a log file that’s three lines away.
If you see lines like these โ and this is the single most common pattern I run into in 502 cases โ stop and read them carefully:
2026/07/21 03:12:11 [error] 27111#27111: *8917 connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.25, server: client-legacy-site.com, request: "GET /index.php HTTP/2.0", upstream: "fastcgi://unix:/run/php/php8.1-fpm.sock:", host: "client-legacy-site.com"
2026/07/21 03:12:12 [error] 27111#27111: *8918 connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.25, server: client-legacy-site.com, request: "GET /index.php HTTP/2.0", upstream: "fastcgi://unix:/run/php/php8.1-fpm.sock:", host: "client-legacy-site.com"
2026/07/21 03:12:12 [error] 27111#27111: *8919 connect() failed (111: Connection refused) while connecting to upstream, client: 198.51.100.7, server: client-legacy-site.com, request: "GET /wp-login.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.1-fpm.sock:", host: "client-legacy-site.com"
2026/07/21 03:12:13 [error] 27111#27111: *8920 connect() failed (111: Connection refused) while connecting to upstream, client: 198.51.100.7, server: client-legacy-site.com, request: "GET /index.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.1-fpm.sock:", host: "client-legacy-site.com"
2026/07/21 03:12:13 [error] 27111#27111: *8921 connect() failed (111: Connection refused) while connecting to upstream, client: 198.51.100.7, server: client-legacy-site.com, request: "GET /index.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.1-fpm.sock:", host: "client-legacy-site.com"

Let me break this log down line by line, because this is where the real skill lives:
- Timestamp (2026/07/21 03:12:11): When it happened. This is gold for cross-referencing with cron jobs or other events running at the same hour. If your 502s always show up at 03:00, suspect a heavy backup or cron job first.
- [error] 27111#27111: The error level and the PID of the Nginx worker that tried to reach the upstream. Handy if you need to trace that process in ps.
- *8917: Nginx’s internal connection number. You can cross-reference it in the access log to see the exact request flow.
- connect() failed (111: Connection refused): This is the money line. Nginx tried to connect to the upstream and nothing was listening. If the upstream is a unix socket, the socket is dead โ php-fpm crashed or never started. If it’s TCP, nothing is listening on that port.
- upstream: “fastcgi://unix:/run/php/php8.1-fpm.sock”: Tells you exactly which upstream is broken. Here it’s crystal clear: fastcgi to the php8.1-fpm socket. A golden clue for the next step.
- server and host fields: The affected domain. I’ve masked the real ones here with a fictional domain, by the way.
From that log, the pattern is hard to miss: an error every single second, all pointing at the php8.1-fpm socket. That means PHP-FPM is down, not merely busy. If it were just busy, you’d see “connection timed out” or “no live upstreams” instead of connection refused. Next stop: PHP-FPM itself. If you want to get better at reading logs generally โ because this skill transfers to every other outage โ I wrote a full piece on it here: How to Read Server Log Files.
Step 2: Check PHP-FPM Status and Logs
Since the Nginx log told us the upstream is PHP-FPM, let’s check on it. First, the service status:
systemctl status php8.1-fpm --no-pager
If you get “inactive (dead)” or worse, “Unit php8.1-fpm.service could not be found,” PHP-FPM is either dead or the version doesn’t match. Check what’s actually installed:
ls /etc/php/
apt list --installed | grep php | grep fpm
Here’s a classic scenario I run into way too often: the Nginx config still points at the php7.4-fpm socket, but PHP was upgraded to 8.1. The socket doesn’t exist, every request fails, 502 everywhere. And the kicker? The Nginx log literally says “unix:/run/php/php8.1-fpm.sock” while the config says php7.4. The answer is right there, but nobody reads the log. It’s frustrating, but also kind of funny โ the log hands you the solution on a plate.
Next, open the PHP-FPM log. If the process died, this file usually holds the reason why. On Ubuntu/Debian it’s typically at /var/log/php8.1-fpm.log:
tail -n 30 /var/log/php8.1-fpm.log
If you see lines like these, PHP-FPM was drowning:
[21-Jul-2026 03:10:12] WARNING: [pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers)
[21-Jul-2026 03:10:13] WARNING: [pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers)
[21-Jul-2026 03:11:02] WARNING: [pool www] server reached pm.max_children setting (50), consider raising it
[21-Jul-2026 03:11:58] NOTICE: [pool www] child 27155 started
[21-Jul-2026 03:12:11] WARNING: [pool www] server reached pm.max_children setting (50), consider raising it
[21-Jul-2026 03:12:12] ERROR: [pool www] child 27122 exited with code 137 (SIGKILL) after 5487.182469 seconds from start
The last line is the one that matters most: “child 27122 exited with code 137 (SIGKILL)”. Exit code 137 means the process was killed forcefully, and in almost every case that’s the OOM killer running out of memory. The warnings above it โ “seems busy” and “server reached pm.max_children” โ already hinted the workers were maxed out. So the chain is complete: workers exhausted, memory running on fumes, OOM killer pulls the trigger, php-fpm collapses, nginx hands out 502s. On to the next step: verify memory and find the OOM killer’s trail.
Step 3: Check Memory and Hunt for OOM Killer Trails
The OOM killer is like the bouncer of a packed club. When the building (RAM) is full, someone gets thrown out โ and it’s usually the biggest process in the room, which is often PHP-FPM. To fix nginx 502 bad gateway errors caused by OOM kills, you need two things: current memory headroom and the kernel’s record of the kill.
First, check how much RAM is left:
free -h
Then check the kernel log for OOM events. On modern Ubuntu/Debian, dmesg usually works:
dmesg -T | grep -i "oom|killed process" | tail -n 20
If dmesg is restricted on your kernel, try journalctl instead:
journalctl -k -b | grep -i "oom|killed process" | tail -n 20
Lines like these are a smoking gun โ the OOM killer executed PHP-FPM:
[Tue Jul 21 03:11:54 2026] php-fpm8.1 invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
[Tue Jul 21 03:11:54 2026] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,totalvm=4G
[Tue Jul 21 03:11:54 2026] Out of memory: Killed process 27122 (php-fpm8.1) total-vm:1024MB, anon-rss:921MB, file-rss:0MB, shmem-rss:0MB
There it is: “Out of memory: Killed process 27122 (php-fpm8.1)” on a box with 4G total. PHP-FPM was burning almost 1GB per worker. A few workers running at once and the whole thing goes sideways. This tells us two things: either the box is too small for the load, or the PHP-FPM config is too greedy. Usually it’s both, and both need fixing so the 502 doesn’t come back tomorrow.
You should also see what else is eating memory. Maybe it’s not PHP-FPM alone โ MySQL and application caches can be the real hogs:
ps aux --sort=-%mem | head -n 20
Typical output (some columns trimmed for readability):
USER PID %CPU %MEM VSZ RSS COMMAND
www-data 27122 2.0 24.3 1048576 921312 php-fpm: pool www
www-data 27140 1.5 22.1 923648 839492 php-fpm: pool www
www-data 27155 1.8 20.9 902144 793280 php-fpm: pool www
root 1284 0.3 12.4 812512 471040 mysqld
root 27001 0.1 4.2 402340 159992 nginx: worker process
If you see several PHP workers each eating 700-900MB, that’s a red flag: either the app has a memory leak or the pool config doesn’t match the machine’s capacity. We’ll fix that in step 5 with proper tuning. If you want to go deeper on spotting resource hogs and load spikes, my article on troubleshooting high load on servers covers exactly that.
Step 4: Verify Socket and Port Match Between Nginx and Upstream
This step makes sure the thing Nginx is pointing at (fastcgi_pass or proxy_pass) actually exists on the box. It’s a frequent culprit โ especially right after a PHP upgrade or after migrating config from another server. Check which upstreams Nginx is using across all site configs:
grep -r "fastcgi_pass|proxy_pass" /etc/nginx/sites-enabled/
Then check where PHP-FPM is listening:
grep -r "listen" /etc/php/8.1/fpm/pool.d/
And verify the socket actually exists with the right permissions:
ls -la /run/php/
Here’s your checklist:
- If Nginx uses
fastcgi_pass unix:/run/php/php8.1-fpm.sock;, then PHP-FPM must listen onlisten = /run/php/php8.1-fpm.sock. Same version, same path, no exceptions. - The socket file must show up in
ls -la /run/php/. If it’s missing, PHP-FPM isn’t running or is listening elsewhere. - The socket needs permissions the nginx worker can use. Usually nginx runs as www-data and the socket is srwxrwxrwx, so it’s fine. If permissions are off, nginx can’t write to it โ hello again, 502.
- If you’re on TCP, say
fastcgi_pass 127.0.0.1:9000;, make sure PHP-FPM listens on that same port and no firewall rule is blocking localhost.
Here’s a real mismatch I’ve seen at a client’s box:
$ grep -r "fastcgi_pass" /etc/nginx/sites-enabled/client-legacy-site.com
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
$ ls /run/php/
php8.1-fpm.pid php8.1-fpm.sock
$ php -v
PHP 8.1.29 (cli) (built: ...)
There it is again. Nginx points at php7.4, but only php8.1 is installed. Different socket, instant 502. The fix is to update the Nginx config, not to downgrade PHP. Get sloppy with version matching during an upgrade and this can hit every site on the box at once.
Step 5: Tune PHP-FPM So It Stops Dying
This is the core step. If the 502 came from maxed-out workers or an OOM kill, the pool config needs tuning. And before touching config, back it up. This isn’t bureaucracy โ it’s what saves you when a typo takes down your pool at 2 AM.
SAFETY WARNING: Back Up Before You Continue
Before editing the PHP-FPM pool config and restarting the service, make sure you have:
- Backed up the config file you’re about to change.
- Verified the backup actually exists and looks correct.
- Confirmed you’re on the right server and the service name is right.
Restarting a production service without a backup can cause longer downtime and lost configuration. Don’t learn this one the hard way.
First, back up the pool config:
cp /etc/php/8.1/fpm/pool.d/www.conf /etc/php/8.1/fpm/pool.d/www.conf.bak
Verify the backup landed:
ls -la /etc/php/8.1/fpm/pool.d/
Then open it in nano or vim:
nano /etc/php/8.1/fpm/pool.d/www.conf
We’re interested in the “Process Manager” section. There are three modes: static, dynamic, and ondemand.
- static: Always keeps the same number of workers, busy or not. Predictable, but wastes RAM on small VPS boxes.
- dynamic: Spins workers up and down between min and max limits. The most common and flexible choice.
- ondemand: Creates workers only when a request arrives and reaps them when idle. Great for RAM savings, but the first request after idle pays a spawn penalty.
Here’s the simple formula I use for a safe max_children:
max_children = (total RAM - OS reserve) / average PHP process size
Example: a 4GB VPS. Set aside 1.5GB for the OS, MySQL, and Nginx, leaving 2.5GB for PHP. From the ps output earlier, each PHP worker averages around 100MB. So:
max_children = (4096 - 1536) / 100 = 25.6 โ 25 workers
Give yourself a little headroom โ never run it right at the edge, because traffic spikes happen. A safe config for a 4GB VPS looks like this:
pm = dynamic
pm.max_children = 25
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 15
pm.max_requests = 1000
Quick explanation of each line:
pm.max_children: the hard cap on workers. This is the critical one โ if it’s exhausted, requests queue up and eventually fail with a 502.pm.start_servers: workers spawned at service start.pm.min_spare_serversandpm.max_spare_servers: the band of idle workers kept ready. Don’t set max_spare too high; you’re just burning RAM.pm.max_requests = 1000: each worker restarts after handling 1000 requests. This is your silent leak defense โ a worker with a slow memory leak gets recycled before it can do real damage.
After editing, validate the config before restarting anything โ PHP-FPM’s version of “nginx -t”:
php-fpm8.1 -t
If it says “test is successful”, you’re clear to restart. If it errors, read the error and fix it first:
systemctl restart php8.1-fpm
Verify it’s actually up and not crash-looping:
systemctl status php8.1-fpm --no-pager
And test a request to the site:
curl -I https://client-legacy-site.com/
If things are back to normal, you’ll see something like:
HTTP/2 200
server: nginx/1.24.0
date: Tue, 21 Jul 2026 03:20:11 GMT
content-type: text/html; charset=UTF-8
HTTP 200. Done. But don’t close the ticket just yet โ the root cause (why memory was exhausted) needs watching. Keep an eye on resources for the next few hours:
watch -n 5 free -h
And confirm the workers aren’t maxing out again:
watch -n 5 systemctl status php8.1-fpm --no-pager | grep -E "Active|Processes"
If you want to make this visible and effortless, set up monitoring. I wrote a practical piece on it here: server monitoring with Netdata โ you don’t need to be a pro to read the graphs.
502 Bad Gateway Troubleshooting Table
Here’s a cheat sheet I keep around. Match your log symptom to the fix:
| Log Symptom | Likely Cause | Quick Fix |
|---|---|---|
| connect() failed (111: Connection refused) to unix socket | PHP-FPM down / crashed / not started | Start service, check OOM, tune pool |
| connect() failed (111: Connection refused) to TCP port | Backend not listening on that port, or firewall blocked | Check ss/netstat, check firewall, align ports |
| connect() failed (2: No such file or directory) to socket | Socket missing / wrong PHP version path | Align fastcgi_pass with pool listen directive |
| upstream timed out (110: Connection timed out) | Backend responding too slowly | Raise fastcgi_read_timeout / proxy_read_timeout |
| server reached pm.max_children setting | All PHP workers exhausted | Raise max_children or optimize queries/code |
| no live upstreams while connecting to upstream | All upstreams in the block marked dead | Check backend health checks, inspect backend app |
| child exited with code 137 (SIGKILL) | OOM killer terminated the process | Check RAM, add swap, tune pool, hunt leaks |
| upstream sent invalid header while reading | Backend returned a malformed response | Check backend app logs, debug the app |
Pro Tips and Warnings From the Trenches
A few things most tutorials skip, straight from the field. These are the difference between finding the fix in ten minutes and chasing your tail for three hours.
1. Know the difference between 502 and 504. 502 means the upstream couldn’t be reached or sent an invalid response. 504 means the upstream was reachable but took too long. If you’re looking at 504, the problem is usually timeout config, not sockets or connections. They get confused all the time, and heading down the wrong path is a great way to burn hours. I’ve written a separate deep dive on it: how to fix Nginx 504 Gateway Timeout.
2. If you use proxy_pass with a hostname, watch out for DNS. Nginx resolves hostnames when the config is loaded, not per request. If your backend is a hostname whose IP changes, Nginx keeps talking to the old IP โ and when that IP dies, boom, 502. Use a resolver 127.0.0.53; directive (or your DNS server) inside the server block, or just point at the IP/port directly.
3. Don’t forget Cloudflare or other CDNs. If your site sits behind Cloudflare, the 502 in the browser might be a 502 from your origin relayed through the edge. Test the origin directly (via a Host header or the IP) so you know where the fault actually is. Don’t blame Nginx when the real problem is a totally different layer.
4. “502 every few hours” is a classic signature. Periodic failures almost always point to a heavy cron job at a specific hour, or a memory leak that creeps up until RAM runs dry. Check the client’s crontab โ if there’s a backup job at 3 AM, suspect it first. Don’t just restart and go back to sleep; you’ll be doing this again tomorrow night.
5. pm.max_requests is a silent lifesaver. Lots of PHP memory leaks go unnoticed simply because workers get recycled by pm.max_requests before they can misbehave. Don’t underestimate a modest value like 500-1000. It’s cheap and the long-term stability payoff is huge.
FAQ: Questions I Get Asked All the Time
Q: Why is my site throwing 502 when the server uptime is high and everything looks fine?
Because a 502 lives at the application/upstream layer, not the whole-server layer. The box can be up for months while PHP-FPM is dead or out of workers. Run systemctl status php8.1-fpm, check the Nginx error log, and check the PHP-FPM log. More often than not, the answer is already sitting there waiting for you.
Q: What’s the difference between 502 Bad Gateway and 504 Gateway Timeout?
502 means Nginx couldn’t get a valid response from the upstream โ connection refused, missing socket, or a garbled response. 504 means the upstream was reachable but didn’t answer within the timeout window (60 seconds by default). For 504, raise fastcgi_read_timeout or proxy_read_timeout. For 502, start at the connection and socket layer.
Q: After changing PHP-FPM config, do I also need to restart Nginx?
Not necessarily. If you only changed the PHP-FPM pool config (www.conf), restarting PHP-FPM is enough. If you touched Nginx config (fastcgi_pass, server blocks, etc.), then reload Nginx. Since you’re handling a 502 that involves both services, the practical move is: make all your config changes first, then restart PHP-FPM and reload Nginx once. Keeps the downtime to a single window.
Q: Why do I get 502 errors every few hours like clockwork?
A periodic pattern like that usually points to one of two things: a heavy cron job at a fixed hour that eats memory until the OOM killer fires, or a slow memory leak that builds up over time. Check dmesg for OOM events, review the crontab, and set pm.max_requests so workers can’t hoard memory forever. Watch resources with Netdata so you can see the pattern instead of guessing.
Q: How do I fix a 502 on a server running cPanel or Plesk?
On panel systems, PHP-FPM is usually managed for you, so the steps shift a bit. Start with the PHP version selector (MultiPHP in cPanel) โ make sure the selected version matches what Nginx expects. Check error logs through the interface or a terminal, and if you need a restart, use the panel’s built-in service restart. Same principle: find the log first, then follow where it points.
Wrapping Up
So that’s the whole journey. If I rewind to that Saturday night in 2019 โ today, when a client calls about a 502, I don’t panic. I just walk the same path: read the Nginx log, identify the upstream, check PHP-FPM status, check memory and OOM, align the socket, tune the pool, done. In most cases the answer shows up by step 2 or 3, and the rest is just confirmation.
If you want to go deeper on reading logs, also check how to read server logs and how to troubleshoot high load โ both will serve you well on cases like this. And if your VPS is chronically low on RAM, read PHP-FPM tuning for low-memory VPS; it’s a lifesaver on small boxes. For the official lowdown on the fastcgi module, the Nginx docs are always worth a read.
Ever hit a 502 with a root cause that made no sense at all? Drop it in the comments โ I’m genuinely curious. The weirdest causes are usually the best stories, and someone else out there probably has the same bug waiting for them. Thanks for reading all the way through, and here’s hoping your 3 AM phone calls stay quiet.