A 502 Bad Gateway from nginx means nginx itself is fine, but the program behind it (PHP-FPM, a Node or Python app, a Docker container) did not give it a usable answer. Nginx tells you exactly which way the answer failed, in one line of its error log, so the fix for a "502 Bad Gateway nginx" page starts with sudo tail -n 20 /var/log/nginx/error.log, not with restarting things at random. Below are the causes in the order we meet them on customer servers, each one broken on purpose on a Debian 13 test server on 24 October 2026, with the exact log line it produced and the change that made the page load again.
The version in the footer of the error page, such as nginx/1.28.3 (Ubuntu), only says which nginx drew the page. It is not the cause, and upgrading nginx will not fix it. Hiding it is one line, server_tokens off; (nginx core module), but do that after the site works.
In HTTP terms, 502 means a server acting as a gateway or proxy "received an invalid response from an inbound server", and 504 means it did not get one in time (RFC 9110, sections 15.6.3 and 15.6.5). Keep that difference in mind; it saves time further down.
Read the log line first: the 502 Bad Gateway nginx cheat sheet
Every row here is a line our test nginx wrote. Find the words from your own log in the left column.
| Words in error.log | What it means | Where to look |
|---|---|---|
(2: No such file or directory) on a unix: socket | The socket file is not there: PHP-FPM is stopped, or the path in fastcgi_pass is wrong (php8.3 vs php8.4) | Upstream down, wrong socket or port |
(111: Connection refused) | Nothing listens there: the app crashed, has not started, or listens on another port or address | Upstream down, wrong socket or port |
(13: Permission denied) | The nginx worker user may not open the socket, or SELinux blocked the connection | Socket permissions, SELinux |
(11: Resource temporarily unavailable) | PHP-FPM's queue is full; every worker is busy | Too few PHP workers |
recv() failed (104: Connection reset by peer) | The backend killed the request halfway, often a PHP-FPM timeout | Timeouts |
upstream prematurely closed connection | The app accepted the request and hung up without answering (crash, out of memory) | Upstream down |
upstream sent too big header | The response headers (usually cookies) do not fit nginx's buffer | Buffers |
upstream timed out (110: Connection timed out) | This is a 504, not a 502 | Timeouts |
The log is at /var/log/nginx/error.log on Debian and Ubuntu unless the site's server block sets its own error_log. To count which errors you have had, as root:
grep -oE "\(([0-9]+): [^)]*\)|upstream (sent too big header|prematurely closed connection|timed out)" /var/log/nginx/error.log | sort | uniq -c
How we tested
One Debian 13 VPS, nginx 1.26.3 and PHP-FPM 8.4.27 from Debian's own packages. We ran a separate nginx instance from its own folder on port 18081, a separate PHP-FPM pool with two workers, and a 30-line Python web app on a Unix socket that can answer normally, answer slowly, hang up, or send a 6,000-byte cookie. Then we broke one thing at a time and requested the page with curl. The log lines below are copied from that server; your paths will be /run/php/... instead of our lab folder, and the rest of each line will look the same.
1. The backend is down
This is most 502s. PHP-FPM was stopped by an update that failed, the Node process exited, the container restarted in a loop, or the server ran out of memory and the kernel killed the biggest process, which is usually the app.
With PHP-FPM stopped, nginx could not even find the socket file, because PHP-FPM removes it on a clean stop:
[crit] 41932#41932: *7 connect() to unix:/root/lab-nginx-502-bad-gateway-fix/run/php.sock failed (2: No such file or directory) while connecting to upstream, client: 127.0.0.1, server: , request: "GET /index.php HTTP/1.1", upstream: "fastcgi://unix:/root/lab-nginx-502-bad-gateway-fix/run/php.sock:"
When we killed the Python app with kill -9, the socket file stayed behind with nobody on the other end, and the error changed:
[error] 41932#41932: *9 connect() to unix:/root/lab-nginx-502-bad-gateway-fix/run/app.sock failed (111: Connection refused) while connecting to upstream
And when the app accepted the request but closed the connection without sending anything, which is what a crash in the middle of a request looks like:
[error] 43044#43044: *15 upstream prematurely closed connection while reading response header from upstream, client: 127.0.0.1, server: , request: "GET /app/crash HTTP/1.1"
Check it
For PHP, as root or with sudo. Replace 8.4 with your version (ls /run/php/ shows it):
systemctl status php8.4-fpm --no-pager
ls -l /run/php/
A healthy Debian 13 server shows Active: active (running) and a socket line that starts with s:
srw-rw---- 1 www-data www-data 0 Oct 24 21:52 php8.4-fpm.sock
If it is not running, start it and read why it stopped:
sudo systemctl start php8.4-fpm
sudo journalctl -u php8.4-fpm -n 20 --no-pager
sudo tail -n 20 /var/log/php8.4-fpm.log
For an app on a TCP port, ask the kernel whether anything listens there. On our server nothing listened on port 3000, and that is exactly what a 111 means:
ss -tlnp | grep ':3000 ' || echo "nothing on 3000"
nothing on 3000
For Unix sockets the same tool lists who owns each one; here both PHP-FPM pools on our test server are alive:
ss -xlp | grep php
u_str LISTEN 0 4096 /run/php/php8.4-fpm.sock 361383 * 0 users:(("php-fpm8.4",pid=40888,fd=10),("php-fpm8.4",pid=40887,fd=10),("php-fpm8.4",pid=40886,fd=8))
If the app keeps dying, the reason is in its own log, not in nginx's. A process that vanishes without a message on a small server is often the kernel's out-of-memory killer; our guide to a slow VPS shows how to spot it, and our measured RAM needs of 34 self-hosted apps help you check whether the plan is simply too small.
Docker adds two traps of its own. The container may show as running while the app inside is still starting, so nginx gets refused for the first seconds after every deploy. And an app that listens on 127.0.0.1 inside its container cannot be reached through a published port at all; it has to listen on 0.0.0.0 inside the container, and the published port (Docker port publishing) is the one nginx connects to.
2. Nginx points at the wrong socket or port
The backend runs fine, just not where nginx looks. Classic cases: PHP was upgraded from 8.2 to 8.4 and the site still says php8.2-fpm.sock; an app moved from port 3000 to 8080; a copied config points at 127.0.0.1:9000 while the pool listens on a socket. The log line is the same as for a dead backend, (2: No such file or directory) for a missing socket or (111: Connection refused) for a closed port:
[error] 41932#41932: *11 connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: , request: "GET /node/ HTTP/1.1", upstream: "http://127.0.0.1:3000/"
The upstream: part at the end of the line is where nginx tried to go. Compare it with what is really listening, then with what your config says. nginx -T prints the whole loaded configuration, includes and all:
sudo nginx -T | grep -nE 'fastcgi_pass|proxy_pass'
grep -E '^listen' /etc/php/8.4/fpm/pool.d/www.conf
On our fresh Debian 13 server, that first command found only two commented examples in the stock default site, and one of them still points to php7.4-fpm.sock, a version Debian 13 does not ship. Uncomment it as it is and you have a 502 on day one.
Make them agree, test, and reload. A reload keeps open connections; a typo is caught by the test before it can take the site down:
sudo nginx -t && sudo nginx -s reload
3. Socket permissions
A Unix socket is a file, and file permissions apply. Debian's PHP-FPM socket belongs to www-data with mode 0660, so only that user and group may connect. Nginx on Debian and Ubuntu runs its workers as www-data too, which is why this works out of the box. It breaks when someone changes user in nginx.conf, or creates a new PHP pool for a site user and forgets the listen.owner lines. We ran our nginx workers as nobody against the stock socket:
[crit] 43010#43010: *1 connect() to unix:/run/php/php8.4-fpm.sock failed (13: Permission denied) while connecting to upstream, client: 127.0.0.1, server: , request: "GET /perm.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.4-fpm.sock:"
Changed back to user www-data; and reloaded, the same request returned 200. Two quick checks, as root:
ps -o user=,cmd= -C nginx
sudo -u www-data test -w /run/php/php8.4-fpm.sock && echo "www-data can use the socket"
Replace www-data in the second line with whatever user the first line shows for the worker processes. If it fails, either run nginx as the right user or tell the pool who may connect. In the pool file, for example /etc/php/8.4/fpm/pool.d/www.conf:
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
The PHP manual notes that on Linux "read/write permissions must be set in order to allow connections from a web server", and that by default owner and group are the user PHP-FPM runs as (PHP-FPM configuration). One more case gives the same error: a socket placed inside /root or a home folder that the nginx user cannot enter. Keep sockets in /run.
4. Too few PHP workers under load
This one only shows up when the site is busy, which makes it the most confusing. Each PHP-FPM worker handles one request at a time, up to pm.max_children. Requests beyond that wait in the socket's queue. If the queue is full too, nginx is turned away and the visitor gets a 502.
We gave the pool 2 workers and a queue of 4, then sent 40 requests at once to a page that takes half a second. 7 got their page, 33 got a 502, each with this line:
[error] 43725#43725: *53 connect() to unix:/root/lab-nginx-502-bad-gateway-fix/run/php.sock failed (11: Resource temporarily unavailable) while connecting to upstream
With the queue back at its default, the same 40 requests all returned 200; they simply waited, and the batch took 10.5 seconds. PHP-FPM wrote a warning worth looking for in your own log:
WARNING: [pool lab] server reached pm.max_children setting (2), consider raising it
sudo grep max_children /var/log/php8.4-fpm.log | tail -n 5
The PHP manual gives a default queue of 511 on Linux; on our Debian 13 server ss showed 4096 for the stock pool, the kernel's net.core.somaxconn value. So in practice a 502 here means the queue was shrunk by hand or the site is far beyond its workers. The fix is more workers, and that means RAM: Debian ships with pm.max_children = 5. Measure what one PHP worker uses on your site with ps -o rss= -C php-fpm8.4 (in KB), divide the RAM you can spare by it, and set that. If the result is still too small, the plan is too small, or pages are too slow and need caching.
5. Timeouts: when it is a 502 and when a 504
Slow backends produce two different errors, and which one you see tells you who gave up.
Nginx gave up waiting: 504. Our app took 10 seconds and the location had proxy_read_timeout 5s. Curl got a 504 after 5.0 seconds:
[error] 43044#43044: *13 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 127.0.0.1, server: , request: "GET /app/slow HTTP/1.1"
The default for proxy_read_timeout and fastcgi_read_timeout is 60 seconds (nginx proxy module). Raised to 15 seconds, the same request returned 200 after 10.0 seconds.
The backend gave up: 502. PHP-FPM can kill a request that runs too long, with request_terminate_timeout in the pool. We set it to 3 seconds and called a script that sleeps for 8. Nginx was still willing to wait, but PHP-FPM killed the worker and the connection broke, so curl got a 502 after 3.9 seconds:
[error] 43044#43044: *7 recv() failed (104: Connection reset by peer) while reading response header from upstream, client: 127.0.0.1, server: , request: "GET /sleep.php HTTP/1.1"
The PHP-FPM log said why:
WARNING: [pool lab] child 43108, script '/root/lab-nginx-502-bad-gateway-fix/html/sleep.php' (request: "GET /sleep.php") execution timed out (3.915790 sec), terminating
With request_terminate_timeout = 10s and a reload of PHP-FPM, the script finished and returned 200 after 8 seconds. Raising timeouts is right for an import or a report that is meant to run long. For a normal page that suddenly needs 30 seconds, the real fix is the slow query or the dead external API behind it, and a longer timeout just keeps visitors waiting longer.
6. Headers too big for nginx's buffer
Nginx reads the response headers into one buffer, by default one memory page: 4 KB on most servers (proxy_buffer_size). An app that sets a big cookie, many cookies, or a long Link or CSP header goes over it. WordPress with some plugins, Magento and single sign-on setups are typical. The page works for a new visitor and breaks after login, which is the clue. Our app set one 6,000-byte cookie:
[error] 43044#43044: *17 upstream sent too big header while reading response header from upstream, client: 127.0.0.1, server: , request: "GET /app/bigcookie HTTP/1.1"
We hit a second trap on the way to the fix: raising only proxy_buffer_size to 16k made nginx refuse the whole configuration:
[emerg] "proxy_busy_buffers_size" must be less than the size of all "proxy_buffers" minus one buffer
Raise the buffers together. For a proxied app:
proxy_buffer_size 16k;
proxy_buffers 8 16k;
For PHP-FPM the same pair is called fastcgi_buffer_size and fastcgi_buffers, and it failed the same way until both were set:
fastcgi_buffer_size 16k;
fastcgi_buffers 8 16k;
After nginx -t passed and nginx reloaded, both the PHP page and the app page with the 6,000-byte cookie returned 200. Put these lines in the location that passes to the backend, not globally, unless every site needs them.
7. SELinux and AppArmor (rarely)
On AlmaLinux, Rocky Linux and RHEL, SELinux stops nginx from opening outgoing connections to an app port by default, so a perfectly running app on port 3000 still gives a 502 and a (13: Permission denied) line, with the real reason in the audit log as a denied name_connect. The fix is the SELinux boolean httpd_can_network_connect, which F5's nginx team describes as allowing "HTTPD scripts and modules to connect to the network using TCP" (Using NGINX with SELinux). Turn that boolean on rather than switching SELinux off.
On Debian and Ubuntu, AppArmor is on but ships no profile for nginx or PHP-FPM, so it is not your cause unless someone added one. Check, as root:
aa-status | grep -iE 'nginx|php' || echo "no nginx or php profile"
Our Debian 13 server printed no nginx or php profile.
Cloudflare 502 or origin 502?
With Cloudflare in front, there are two gateways, and either one can say 502. Cloudflare's documentation splits them by the page you see: a Cloudflare-branded 502 or 504 page means your origin server returned the error, while a blank page without Cloudflare branding points to Cloudflare's side, for example a compression problem at the origin or traffic being moved between its data centres (Cloudflare: Error 502 or 504). A page that says nginx at the bottom came from your server, and everything above applies.
To see who answered, look at the headers. Through Cloudflare you get server: cloudflare and a cf-ray ID, as we saw testing against cloudflare.com:
curl -sI https://www.cloudflare.com | grep -iE '^(HTTP|server|cf-ray)'
HTTP/2 200
server: cloudflare
cf-ray: a4fc3bff5a23b0ce-SKP
Then skip Cloudflare and ask your server directly. --resolve sends the request to the IP you give while keeping the right host name, so nginx picks the right site. On our test server, for a made-up name on the lab port:
curl -s -o /dev/null -w "%{http_code}\n" --resolve shop.example.test:18081:127.0.0.1 http://shop.example.test:18081/index.php
200
For your site, use your domain, port 443 and your server's public IP with https://. If the server answers 200 directly and only Cloudflare shows 502, look at the Cloudflare side: SSL mode, a firewall that blocks Cloudflare's IP ranges, or the cases on the page linked above. If the server returns 502 directly too, the cause is on your server. Our reverse proxy and free HTTPS guide covers the certificate side, plus the localhost resolving to ::1 case that catches apps listening on IPv4 only.
Frequently asked questions
What does 502 Bad Gateway nginx mean?
It means nginx is running but the backend it forwards requests to, such as PHP-FPM, a Node or Python app or a Docker container, did not return a valid response. The nginx error log, usually /var/log/nginx/error.log, has one line per failed request that names the reason, for example Connection refused or upstream sent too big header.
How do I fix 502 Bad Gateway in nginx with PHP-FPM?
Check that PHP-FPM runs with systemctl status php8.4-fpm, then compare the socket in fastcgi_pass with the listen line of the pool and the real file in /run/php/. If both match and it still fails, check socket permissions, pm.max_children under load and fastcgi_buffer_size for large cookies.
Is 502 Bad Gateway my fault or the website's?
If you are visiting someone else's site, it is the site's problem; reloading after a minute is all you can do. If it is your own server, the cause is on the server, almost always the application behind nginx, not your browser or connection.
What is the difference between 502 and 504 in nginx?
A 504 means nginx waited for the backend longer than its timeout (60 seconds by default) and gave up. A 502 means the backend refused, broke or garbled the answer. In our test, a 10-second response with a 5-second proxy_read_timeout was a 504, while PHP-FPM killing a script after 3 seconds was a 502.
Will restarting nginx fix a 502?
Rarely. Nginx is the part that works. Restarting the backend (PHP-FPM, the app, the container) often brings the site back for a while, but if it died from lack of memory or a crash, it will happen again until the reason in its own log is fixed.
Why do I get 502 only after logging in?
Usually the response headers grew past nginx's 4 KB header buffer because of session or plugin cookies. The log says upstream sent too big header. Raise proxy_buffer_size and proxy_buffers (or the fastcgi_ versions) together, as shown above.
Still stuck?
Most 502s are solved by the first two sections: something is not running, or nginx looks in the wrong place. If yours shows up only under load, it is usually workers and memory, and that is where the server size matters. Our KVM VPS and VDS plans in Amsterdam, Dublin and Prishtina run on NVMe with full root access; VPS plans use shared vCPUs, VDS plans dedicated vCPUs, which helps a busy PHP site whose workers all want CPU at the same moment. If you want a second pair of eyes on a log line, send it to us on Telegram, and for hardening the rest of a fresh server, our Debian 13 security checklist is the next step.