Updated
Nginx 502 Bad Gateway: find the upstream error and fix it on Ubuntu
Short answer: Nginx shows 502 Bad Gateway when the upstream it proxies to (your app, PHP-FPM, Gunicorn, a container) refused the connection, closed it without answering, or sent a response nginx couldn't accept. Nginx itself is usually fine. Run sudo tail -n 50 /var/log/nginx/error.log right after a failing request: the line logged at that moment names the cause, and the upstream: field shows exactly where nginx tried to connect.
This guide is for anyone running nginx as a reverse proxy on Ubuntu 22.04 or 24.04. It starts with a table of real nginx error messages, then walks through the checks in order. The commands in the walkthrough only read state; the changes are collected at the end. Run them only on servers you're authorized to access.
502 bad gateway nginx
Nginx is the gateway. A 502 means nginx accepted the visitor's request, passed it on, and got nothing usable back. The common causes:
- Nothing is listening where
proxy_pass/fastcgi_passpoints: the app is stopped, crashed, still starting, or moved to another port or socket. - Nginx isn't allowed to connect: wrong owner or mode on a Unix socket.
- The app dropped the request: it crashed, was killed (out of memory, worker timeout), or restarted mid-request.
- The app answered with something nginx rejects: malformed or oversized response headers, or the wrong protocol (plain HTTP vs HTTPS).
If the app answers but too slowly, nginx returns 504 Gateway Timeout instead. Telling 502 from 504 already halves the search.
Match your error.log line
Find the line with the same timestamp as the failing request. The first column is the text nginx writes; addresses, socket paths and PHP versions will differ on your server. Each line ends with context such as client: ..., server: ..., request: "...", upstream: "...", host: "...".
| What error.log says | Status | Likely cause | Next check |
|---|---|---|---|
connect() failed (111: Connection refused) while connecting to upstream | 502 | Nothing listens on the upstream: address: app stopped, crashed, still starting, or on another port | Step 4, Step 5 |
connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory) while connecting to upstream | 502 | The socket file doesn't exist: PHP-FPM (or the app) is stopped, or nginx points at another version's socket | ls -l /run/php/; Step 5 |
connect() to unix:/run/php/php8.3-fpm.sock failed (13: Permission denied) while connecting to upstream | 502 | The nginx user (www-data on Ubuntu) can't open the socket | Step 7 |
connect() to unix:/run/php/php8.3-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream | 502 | The socket's listen queue is full: every worker is busy and the backlog overflowed | PHP-FPM log for max_children; Step 8 |
upstream prematurely closed connection while reading response header from upstream | 502 | The app accepted the request, then closed the connection without a response: crash, killed worker, app-server timeout | Step 8 |
recv() failed (104: Connection reset by peer) while reading response header from upstream | 502 | The upstream reset the connection: process crashed or restarted, worker terminated, or a reused keepalive connection was already closed | Step 8; keepalive |
upstream prematurely closed FastCGI stdout while reading response header from upstream | 502 | A PHP-FPM worker ended before sending output (fatal error, crash or kill) | PHP-FPM log and PHP error log |
upstream sent invalid header: "..." while reading response header from upstream | 502 | The app sent a malformed response header line | Invalid response |
upstream sent too big header while reading response header from upstream | 502 | Response headers (often large Set-Cookie) don't fit in proxy_buffer_size (fastcgi_buffer_size for PHP) | Buffers |
SSL_do_handshake() failed (SSL: error:...:wrong version number) while SSL handshaking to upstream | 502 | proxy_pass https:// points at a port that speaks plain HTTP | Use http:// for that upstream, or point at its TLS port |
no live upstreams while connecting to upstream | 502 | Every server in the upstream {} group is marked failed after earlier errors | Scroll up: the earlier lines show why each server failed |
connect() failed (113: No route to host) while connecting to upstream | 502 | Upstream IP unreachable: wrong IP, host or container gone, or a firewall rejecting it | ip route get UPSTREAM_IP; Docker |
upstream timed out (110: Connection timed out) while reading response header from upstream | 504 | The app didn't answer within proxy_read_timeout (60 s by default) | Slow requests in the app log; timeouts |
Two lookalikes that aren't 502s:
host not found in upstream "app:3000"is logged at[emerg]when nginx loads its configuration. Nginx refuses to start or reload, so visitors get a connection error, not a 502.sudo nginx -tshows it.connect() to 127.0.0.1:3000 failed (13: Permission denied)on a TCP address is the classic SELinux block on RHEL, Rocky or Alma. Ubuntu doesn't use SELinux; see the AppArmor note in Step 7.
Nginx logs "upstream sent invalid header"; "The proxy server received an invalid response from an upstream server" is Apache
Nginx doesn't write the phrase "upstream sent invalid response". When the upstream's reply is broken, the error log uses one of these, all of which end in a 502:
upstream sent invalid header: "...": a response header line nginx can't parse, such as a bad character in a header name or a missing colon. Nginx 1.24 (Ubuntu 24.04) quotes the start of the bad line; nginx 1.18 (Ubuntu 22.04) logs onlyupstream sent invalid header.upstream sent too big header: the status line plus headers are bigger than the buffer. Often a session cookie that keeps growing.upstream sent invalid "Content-Length" header: "..."andupstream sent "Content-Length" and "Transfer-Encoding" headers at the same time: the app or a framework middleware is building responses by hand and getting them wrong.
To see exactly what the app sends, skip nginx and look at the raw headers (Step 6):
curl -sS -o /dev/null -D - --max-time 10 -H 'Host: example.com' http://127.0.0.1:3000/PATH
If you saw "The proxy server received an invalid response from an upstream server", that page comes from Apache mod_proxy, not nginx. Check Apache's error log (/var/log/apache2/error.log on Ubuntu).
Fix 502 Bad Gateway on Ubuntu: step-by-step walkthrough
Replace example.com, YOUR_APP, port 3000 and socket paths with your own. Ubuntu 22.04 ships PHP 8.1 and 24.04 ships PHP 8.3, so adjust php8.3-fpm to match.
Step 1: Confirm the 502 comes from your nginx
curl -sS -o /dev/null -D - https://example.com/ | grep -i -E '^(HTTP|server|cf-ray)'
curl -sS -o /dev/null -w '%{http_code}\n' --resolve example.com:443:127.0.0.1 https://example.com/
The first request goes through DNS and any CDN. A server: cloudflare header or a cf-ray header means a CDN answered; its 502 page means the CDN couldn't get a good answer from your server. The second request, run on the server, sends the same request straight to the local nginx and skips the CDN (use http:// and port 80 if the site has no TLS). If that also returns 502, keep going. If it returns 200, the problem sits between the CDN and your server, not behind nginx.
Nginx's own error page shows 502 Bad Gateway with a footer like nginx/1.24.0 (Ubuntu). The footer is just the version string; upgrading nginx won't fix a 502.
Step 2: Check that nginx is running and its configuration is valid
systemctl status nginx --no-pager
sudo nginx -t
active (running) plus syntax is ok and test is successful means nginx itself is healthy, and the 502 is about the upstream. If nginx -t fails, the running nginx may still be serving an older configuration; fix the error it names before you reload anything.
Step 3: Read the error log at the moment of the 502
sudo tail -n 50 /var/log/nginx/error.log
sudo grep -E 'upstream|connect\(\)' /var/log/nginx/error.log | tail -n 20
sudo awk '$9 == 502' /var/log/nginx/access.log | tail -n 5
A server block can have its own log file. To find it, and where each location proxies to:
sudo nginx -T 2>/dev/null | grep -n -E 'error_log|access_log|proxy_pass|fastcgi_pass|uwsgi_pass|upstream'
nginx -T prints the whole loaded configuration and can contain credentials, so don't paste it publicly. To catch an intermittent 502, keep sudo tail -f /var/log/nginx/error.log open in one terminal and reproduce the request in another.
A full line looks like this:
2026/10/08 14:03:12 [error] 1234#1234: *5678 connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.10, server: example.com, request: "GET /api/orders HTTP/1.1", upstream: "http://127.0.0.1:3000/api/orders", host: "example.com"
upstream:is the exact address nginx tried. Compare it with where the app really listens; many 502s are solved right here.request:tells you whether every path fails or only one route.- The timestamp is in the server's local time.
Step 4: Check the upstream service and its logs
For an app run by systemd (Node, Gunicorn, uWSGI, Puma and so on):
systemctl status YOUR_APP --no-pager
sudo journalctl -u YOUR_APP --since "30 min ago" --no-pager | tail -n 50
For PHP-FPM:
systemctl status php8.3-fpm --no-pager
sudo journalctl -u php8.3-fpm --since "30 min ago" --no-pager | tail -n 30
sudo tail -n 50 /var/log/php8.3-fpm.log
For a container:
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
docker logs --since 30m --timestamps YOUR_CONTAINER 2>&1 | tail -n 50
Look for the service being stopped, in a restart loop (activating (auto-restart), Restarting (1)), or logging a crash at the time of the 502. A container that keeps exiting has its own guide: Docker exit code 1, 137, 139, 255: why a container exits immediately.
Step 5: Check that something is listening where nginx connects
sudo ss -ltnp
sudo ss -lxp | grep -E 'php|gunicorn|uwsgi|\.sock'
ls -l /run/php/
ss -ltnp lists listening TCP ports and the process holding each one; ss -lxp does the same for Unix sockets. Compare them with the upstream: field from Step 3:
- App on
3001, nginx sending to3000: port mismatch, often after a deploy or a changedPORTvariable. proxy_pass http://localhost:3000while the app listens only on127.0.0.1: nginx may resolvelocalhostto both127.0.0.1and[::1], and the IPv6 attempt is refused (you'll seeupstream: "http://[::1]:3000/..."in the log). Writing127.0.0.1inproxy_passremoves the ambiguity./run/php/php8.1-fpm.sockexists but nginx usesphp8.3-fpm.sock: a PHP upgrade left the old socket path in the site config.
Step 6: Call the upstream directly, bypassing nginx
curl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' --max-time 10 -H 'Host: example.com' http://127.0.0.1:3000/
curl -sS -o /dev/null -w '%{http_code}\n' --max-time 10 --unix-socket /run/gunicorn.sock http://localhost/
Use a path that is safe to request. The second form is for an HTTP app behind a Unix socket (Gunicorn, Puma, uWSGI in HTTP mode).
- Connection refused here too: the app isn't serving; go back to Step 4.
- 200 here but 502 through nginx: compare the address you tested with the
upstream:field; check socket permissions (Step 7) and response headers (invalid response). - It hangs for many seconds: a slow app, which through nginx usually becomes a 504.
PHP-FPM speaks FastCGI, not HTTP, so curl can't test it. If ping.path is enabled in the pool file, cgi-fcgi from the libfcgi-bin package can call it:
SCRIPT_NAME=/ping SCRIPT_FILENAME=/ping REQUEST_METHOD=GET cgi-fcgi -bind -connect /run/php/php8.3-fpm.sock
Otherwise, a running service plus a socket file that www-data can open (Step 7) is the practical check.
Step 7: Check Unix socket permissions
On Ubuntu, nginx workers run as www-data. They need write permission on the socket and execute permission on every directory above it.
ps -o user= -C nginx | sort | uniq -c
ls -l /run/php/php8.3-fpm.sock
sudo grep -E '^\s*(user|group|listen(\.owner|\.group|\.mode)?)\s*=' /etc/php/8.3/fpm/pool.d/*.conf
namei -l /run/gunicorn.sock
- The stock pool sets
listen.owner = www-data,listen.group = www-dataandlisten.mode = 0660. A pool copied from another server or edited for a different user often breaks this. namei -lshows owner and mode for each directory in the path. A socket under/home/USER/...fails if the home directory isn't traversable bywww-data.- A socket in
/tmpcan be invisible to nginx if the app's unit hasPrivateTmp=yes(systemctl show -p PrivateTmp YOUR_APP). Nginx then logs(2: No such file or directory)even though the app "created" it. Put sockets under/run/instead. - AppArmor: Ubuntu uses AppArmor, not SELinux, and the Ubuntu nginx package doesn't confine nginx with an AppArmor profile by default.
sudo aa-status | grep -i nginxprints nothing unless someone added one. Without a profile,(13: Permission denied)on Ubuntu is a file-permission problem.
Step 8: Check whether the app was killed or timed out
upstream prematurely closed connection and Connection reset by peer usually mean the worker handling the request died. Check, for the same minute as the 502:
sudo journalctl -k --since "1 hour ago" --no-pager | grep -i -E 'out of memory|killed process'
sudo journalctl -u YOUR_APP --since "1 hour ago" --no-pager | grep -i -E 'worker timeout|killed|signal|error'
sudo grep -i -E 'timed out|max_children|exited on signal' /var/log/php8.3-fpm.log | tail -n 20
- Kernel OOM killer:
Out of memory: Killed process ...naming your app or a worker. More memory isn't automatically the fix; find what grew. See Exit code 137 and OOMKilled for the container case. - Gunicorn:
[CRITICAL] WORKER TIMEOUTmeans Gunicorn killed a sync worker that ran longer than its--timeout(30 s by default), and nginx sees the connection close. Raising nginx's timeouts doesn't help; the app server cut the request first. - PHP-FPM:
execution timed out ... terminatingcomes fromrequest_terminate_timeout.server reached pm.max_childrenmeans requests are queuing; if the queue overflows, nginx logs(11: Resource temporarily unavailable).
Docker: 127.0.0.1 is not your app
In an nginx container, proxy_pass http://127.0.0.1:3000; points at the nginx container itself, and you get (111: Connection refused). The usual fixes, each a change to review first:
- On the same Compose network, proxy to the service name:
proxy_pass http://app:3000;. - The app must listen on
0.0.0.0inside its container, not127.0.0.1. - With nginx on the host, publish the app's port to loopback (for example
127.0.0.1:3000:3000) and proxy to that.
Nginx resolves hostnames in proxy_pass when it loads its configuration. If the app container is recreated with a new IP, nginx can keep using the old one and log (113: No route to host) or (111: Connection refused) until it is reloaded. In the official nginx image the error log goes to stderr: docker logs --since 15m YOUR_NGINX_CONTAINER 2>&1 | grep -i upstream.
Proxy timeouts, buffers and keepalive: change them only when the log points there
Timeouts give 504, not 502. proxy_connect_timeout, proxy_send_timeout and proxy_read_timeout all default to 60 s. Raise proxy_read_timeout only for a specific location that's slow by design, such as a report export, and find out why the request is slow first.
upstream sent too big header needs bigger buffers. proxy_buffer_size defaults to one memory page (4k on x86_64). Raise it together with proxy_buffers:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_buffer_size 16k;
proxy_buffers 8 16k;
}
Raising proxy_buffer_size alone to 16k makes nginx -t fail with "proxy_busy_buffers_size" must be less than the size of all "proxy_buffers" minus one buffer, because the busy-buffers default grows with the larger buffer. For PHP-FPM, the equivalents are fastcgi_buffer_size and fastcgi_buffers. If headers keep growing, fix the cookie or header that's growing too.
Occasional 502s with upstream keepalive. If your upstream {} block uses keepalive, nginx reuses idle connections. If the app closes idle connections sooner than nginx expects, a request can land on a connection the app just closed, and nginx logs upstream prematurely closed connection or (104: Connection reset by peer). Node.js, for example, closes idle keep-alive connections after 5 seconds by default (server.keepAliveTimeout). Make the app's idle timeout longer than keepalive_timeout in the upstream block (60 s by default), or lower the latter.
Apply the fix safely
Starting a stopped app, correcting proxy_pass or a socket path, fixing socket ownership, or changing buffers are all changes:
- Copy the file first, for example
sudo cp /etc/nginx/sites-available/example.com /root/example.com.bak. - Make the one change the log points to.
sudo nginx -t, thensudo systemctl reload nginx(reload keeps current connections; restart drops them).- Repeat the failing request and watch
sudo tail -f /var/log/nginx/error.log. - If it's no better, put the backup back, test and reload again.
Don't raise every timeout "just in case", and don't open up socket permissions to 0666; fix the cause the log names.
Illustrative example
Illustrative example, not a customer incident: after a server is upgraded from Ubuntu 22.04 to 24.04 (PHP 8.1 to 8.3), every PHP page returns 502. The error log shows connect() to unix:/run/php/php8.1-fpm.sock failed (2: No such file or directory) while connecting to upstream. ls -l /run/php/ lists only php8.3-fpm.sock, and systemctl status php8.1-fpm says the unit isn't found. That supports a stale socket path in the site config. A person decides the fix: change fastcgi_pass to the new socket (after checking the PHP extensions the site needs are installed for 8.3), run nginx -t, reload, and retest.
AI diagnostic prompt
This site returns 502 Bad Gateway. Without changing anything, find the nginx error_log in use, show upstream errors from the last 15 minutes, compare the upstream: addresses with what is actually listening (ports, Unix sockets, containers), check the upstream service's status and logs, and test the upstream directly from the server. Separate facts from hypotheses. Don't restart services or edit configuration; propose the smallest change with its risk, how to verify it and how to roll it back, and wait for my approval.
References: nginx proxy module, nginx upstream module, nginx fastcgi module.
If the site went down overnight and the 502 is only one symptom, use that 15-minute triage before you change anything. A suspicious SSH login in the same window is a different problem: preserve the evidence instead of restarting to clear the 502.
Related guides
- Site down at 2 a.m.: the first 15 minutes when you're the only one on call
- Was my Linux server hacked? Read-only checks for suspicious logins and processes
- Docker exit code 1, 137, 139, 255: why a container exits immediately
- Exit code 137 and OOMKilled: out of memory or something else?
FAQ
What causes "502 Bad Gateway" in nginx, and how do I fix it?
A 502 means nginx accepted the request, passed it to the upstream (your app, PHP-FPM, Gunicorn or a container) and got nothing usable back: the upstream refused the connection, closed it without answering, or sent a response nginx couldn't accept. Nginx itself is usually fine. Run sudo tail -n 50 /var/log/nginx/error.log right after a failing request: that line names the cause, and the upstream: field shows exactly where nginx tried to connect.
Is "The proxy server received an invalid response from an upstream server" an nginx error?
No. That page comes from Apache mod_proxy, so check Apache's error log (/var/log/apache2/error.log on Ubuntu). Nginx doesn't write the phrase "upstream sent invalid response" either. When an upstream's reply is broken, nginx logs upstream sent invalid header, upstream sent too big header or a Content-Length error instead, and all of them end in a 502.
What does "connect() failed (111: Connection refused) while connecting to upstream" mean?
Nothing is listening on the upstream: address: the app is stopped, crashed, still starting, or on another port. Check the upstream service with systemctl status or docker ps -a and its logs, then compare sudo ss -ltnp with the upstream: field. If nginx runs in a container, proxy_pass http://127.0.0.1:3000 points at the nginx container itself; on the same Compose network, the usual fix is to proxy to the service name.
What's the difference between 502 and 504, and should I raise nginx timeouts for a 502?
A 502 means the upstream refused the connection, dropped it, or sent an invalid reply. A 504 Gateway Timeout means the app answered too slowly, beyond proxy_read_timeout (60 s by default). Timeouts give 504, not 502, so raising them rarely fixes a 502; with a Gunicorn WORKER TIMEOUT, for example, the app server cut the request first. Raise timeouts only for a location that's slow by design, after finding out why.
Need help interpreting the evidence?
OpsMate (itops.sh) puts an SSH terminal and an AI assistant on one page: type the commands above yourself, or describe the 502 in plain language and compare the answer with the raw output. Command output you send to the AI is processed in the cloud, so redact secrets first. Free plan: 500 AI requests a month, unlimited servers.
Start free Desktop with local credentials