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:

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 saysStatusLikely causeNext check
connect() failed (111: Connection refused) while connecting to upstream502Nothing listens on the upstream: address: app stopped, crashed, still starting, or on another portStep 4, Step 5
connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory) while connecting to upstream502The socket file doesn't exist: PHP-FPM (or the app) is stopped, or nginx points at another version's socketls -l /run/php/; Step 5
connect() to unix:/run/php/php8.3-fpm.sock failed (13: Permission denied) while connecting to upstream502The nginx user (www-data on Ubuntu) can't open the socketStep 7
connect() to unix:/run/php/php8.3-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream502The socket's listen queue is full: every worker is busy and the backlog overflowedPHP-FPM log for max_children; Step 8
upstream prematurely closed connection while reading response header from upstream502The app accepted the request, then closed the connection without a response: crash, killed worker, app-server timeoutStep 8
recv() failed (104: Connection reset by peer) while reading response header from upstream502The upstream reset the connection: process crashed or restarted, worker terminated, or a reused keepalive connection was already closedStep 8; keepalive
upstream prematurely closed FastCGI stdout while reading response header from upstream502A 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 upstream502The app sent a malformed response header lineInvalid response
upstream sent too big header while reading response header from upstream502Response 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 upstream502proxy_pass https:// points at a port that speaks plain HTTPUse http:// for that upstream, or point at its TLS port
no live upstreams while connecting to upstream502Every server in the upstream {} group is marked failed after earlier errorsScroll up: the earlier lines show why each server failed
connect() failed (113: No route to host) while connecting to upstream502Upstream IP unreachable: wrong IP, host or container gone, or a firewall rejecting itip route get UPSTREAM_IP; Docker
upstream timed out (110: Connection timed out) while reading response header from upstream504The 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:

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:

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"

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:

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

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

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

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:

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:

  1. Copy the file first, for example sudo cp /etc/nginx/sites-available/example.com /root/example.com.bak.
  2. Make the one change the log points to.
  3. sudo nginx -t, then sudo systemctl reload nginx (reload keeps current connections; restart drops them).
  4. Repeat the failing request and watch sudo tail -f /var/log/nginx/error.log.
  5. 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

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

Troubleshooting guides