Troubleshooting

502 Bad Gateway: what it means and how to fix it

A 502 Bad Gateway means a proxy got an invalid response, or none, from the app behind it. Who shows it, what the log says, and how to fix each cause.

Published ~10 min read
On this page

Short answer: a 502 Bad Gateway error means a gateway or proxy (nginx, Apache, a load balancer or a CDN) got an invalid response, or none at all, from the server behind it. The proxy works; the application behind it does not. Visitors can reload and wait. Site owners find the cause in the proxy’s error log.

Debian · Ubuntu · on the server, right after a 502
# 1. What did the proxy see?
sudo tail -n 20 /var/log/nginx/error.log          # Apache: /var/log/apache2/error.log

# 2. Is the backend running, and listening where the proxy expects?
sudo systemctl status php8.3-fpm --no-pager       # or your app's service
sudo ss -tlnp | grep ':3000'                      # TCP backends
sudo ss -xlp | grep -E 'php|gunicorn|sock'        # Unix sockets

# 3. Ask the backend directly, without the proxy
curl -sS -o /dev/null -w '%{http_code} in %{time_total}s\n' http://127.0.0.1:3000/

Replace php8.3-fpm with the PHP version you run (ls /run/php/ shows it) and 3000 with the port from your proxy_pass. If the backend answers directly but not through the proxy, the problem is the address, the socket or the permissions between the two.

What does 502 Bad Gateway mean?

RFC 9110 defines 502 as a server that, “while acting as a gateway or proxy, received an invalid response from an inbound server it accessed while attempting to fulfill the request”. In practice most proxies also use it when they got no response at all: the connection was refused, the socket did not exist, or the backend closed the connection before it answered.

The important part is who sends it. A 502 is written by the proxy in front, about the server behind it. When your application crashes it does not send a 502; it stops answering, and nginx or the load balancer reports that. (An app that calls another service can pass on a 502 of its own, and then its own log says so.) Browsers show the proxy’s page, which is why the same error appears as 502 Bad Gateway, HTTP Error 502, 502 Proxy Error or a branded error page, depending on what sits in front.

Who shows the 502?

What you seeSent byWhat it usually means
White page, 502 Bad Gateway, footer nginxnginxBackend down, wrong socket or port, or it closed the connection
Proxy Error. The proxy server received an invalid response from an upstream server.Apache mod_proxyThe backend's answer broke off or was not HTTP (AH01102)
502 Bad Gateway. The server returned an invalid or incomplete response.HAProxyThe server in the backend sent nothing usable
Empty page with status 502CaddyIts log names the dial error, such as connect: connection refused
Bad GatewayTraefikNo answer from the service or container
HTTP Error 502.3 or 502.5 Process FailureIIS (ARR or the ASP.NET Core module)The backend failed, timed out, or the app did not start
Cloudflare-branded 502 pageCloudflare's own networkA problem inside Cloudflare; check its status page
Plain 502 page while the site runs behind CloudflareYour origin's proxy, passed throughEverything on this page applies

Apache and HAProxy differ from nginx here: when they cannot connect to the backend at all, they answer 503 Service Unavailable (AH01114 in Apache’s log), and keep 502 for a backend that answered with something broken. nginx and Caddy use 502 for both.

Cloudflare: its own 502, or one of 520 to 526

When Cloudflare itself cannot get a usable answer from your origin, it uses its own codes rather than 502. Per Cloudflare’s documentation, a Cloudflare-branded 502 or 504 points at its own network, while a plain, unbranded page came from your server and was passed through. The origin codes each have a guide:

CodeWhat Cloudflare saw
520The origin answered with something empty, reset or malformed
521The origin refused the connection: web server stopped or port closed
522The TCP connection to the origin timed out
523The origin is unreachable: wrong IP, routing or DNS
524Connected, but no HTTP response in time
525The TLS handshake with the origin failed
526The origin's certificate is invalid in Full (strict) mode

How do I fix a 502 error as a visitor?

  • Reload after a minute. Many 502s last only while an app restarts or a deploy switches over. Reloading every second adds load and does not help.
  • Check other sites. If every site shows 502, the gateway is on your side: a corporate proxy, a VPN, or a security product that inspects traffic. Try without it.
  • Do not bother with cookies and cache. A 502 happens between two servers, so clearing them rarely changes anything. The exception is a site that just moved servers, where flushing the DNS cache gets you to the new address.
  • Tell the owner if it stays. Note the time and the URL. That is enough for them to find the line in their log.

Site owner: what the error log says

The proxy writes one line per failed request, and its wording names the cause. These are nginx’s messages, the ones you meet most on a VPS:

Log messageCauseFix
connect() failed (111: Connection refused) while connecting to upstreamNothing listens on that address and port: the app is down, crashed, or listens elsewhereStart it, check the port
connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory)The socket path is wrong, often after a PHP upgrade, or FPM is stoppedFix the path
(13: Permission denied) while connecting to upstreamnginx may not open the socket, or SELinux blocks the connectionSocket owner or SELinux boolean
upstream prematurely closed connection while reading response header from upstreamThe backend died or killed the request before it answeredCrash, OOM kill or worker timeout
recv() failed (104: Connection reset by peer) while reading response header from upstreamThe same family: the connection was reset mid-requestCheck the backend’s log
upstream sent too big header while reading response header from upstreamResponse headers (usually cookies) exceed the proxy bufferRaise the buffer size
no live upstreams while connecting to upstreamEvery server in the upstream block was marked failedFix the backends; nginx retries them after fail_timeout

With PHP-FPM, the closed-connection case reads upstream prematurely closed FastCGI stdout. Apache’s counterpart is AH01102: error reading status line from remote server. The nginx 502 guide keeps the nginx-only checklist short; the causes below go into each case for any proxy.

Test the upstream directly

Skipping the proxy splits the problem in two. Ask the backend on the server itself, with the exact address from your config:

on the server · HTTP backends
# TCP: Node, gunicorn, uvicorn, another web server
curl -sS -i http://127.0.0.1:3000/ | head -n 5

# Unix socket, for example gunicorn on /run/gunicorn.sock
curl -sS -i --unix-socket /run/gunicorn.sock http://localhost/ | head -n 5

If the socket file does not exist, curl 7.88.1 on Debian 12 prints a misleading line, naming port 80 although it never used the network:

Output, missing socket
curl: (7) Failed to connect to localhost port 80 after 0 ms: Couldn't connect to server

PHP-FPM speaks FastCGI, not HTTP, so curl cannot test it. cgi-fcgi from the libfcgi-bin package can, against FPM’s ping page. Enable it in the pool with ping.path = /ping, restart FPM, then:

Debian · Ubuntu · PHP-FPM health check
sudo apt install -y libfcgi-bin
sudo env SCRIPT_NAME=/ping SCRIPT_FILENAME=/ping REQUEST_METHOD=GET \
  cgi-fcgi -bind -connect /run/php/php8.3-fpm.sock
# a few header lines, then: pong

An answer here and a 502 through nginx means the backend is fine and the path between them is not. No answer here means the proxy is right to complain.

Cause 1: the backend is not running

Connection refused means nothing listens on that address. Look at the service and its own log, which journalctl reads:

Debian · Ubuntu · service state and its last messages
sudo systemctl status myapp --no-pager
sudo journalctl -u myapp -n 50 --no-pager
sudo journalctl -u myapp --since "15 min ago" -p err
systemctl show myapp -p NRestarts         # automatic restarts so far

Active: activating (auto-restart) with status=1/FAILURE is a crash loop: systemd restarts the app, it dies at start-up, and every request in between gets a 502. The journal names the reason, typically a missing environment variable, a syntax error after a deploy, a database it cannot reach, or a port already taken (see address already in use). Fix that, then sudo systemctl restart myapp.

Cause 2: the proxy points at the wrong address or socket

  • A PHP upgrade moved the socket. php8.2-fpm.sock becomes php8.3-fpm.sock, and fastcgi_pass still names the old one. ls /run/php/ shows what exists.
  • localhost is not one address. proxy_pass http://localhost:3000 resolves to both ::1 and 127.0.0.1, and nginx tries the other one when the first refuses, so the log fills with refused attempts on the address the app does not use. A real 502 follows when the two sides name different addresses: nginx proxies to 127.0.0.1 while a Node app (version 17 or later) bound to localhost listens only on ::1. The upstream: field in the log line shows which address failed. Use the same literal address on both sides.
  • A container got a new IP. Proxying to a container’s internal IP breaks after a restart. Publish the port on 127.0.0.1 and proxy to that, or put the proxy in the same Docker network and use the service name.
compare what the proxy wants with what listens
sudo nginx -T 2>/dev/null | grep -nE 'proxy_pass|fastcgi_pass|server .*:'
sudo ss -tlnp
ls -l /run/php/

Cause 3: socket permissions and SELinux

(13: Permission denied) on a Unix socket means the nginx worker user may not open it. Debian’s PHP-FPM pool creates the socket for www-data with mode 0660, which fits Debian’s nginx. The packages from nginx.org run nginx as the user nginx instead, and then the socket is closed to it. Align the pool with the user nginx runs as:

/etc/php/8.3/fpm/pool.d/www.conf
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = nginx        ; the group of the nginx worker user
listen.mode = 0660

Restart FPM afterwards. On Rocky, AlmaLinux and RHEL, the same error for a TCP backend such as 127.0.0.1:3000 is usually SELinux, which stops the web server from opening network connections until you allow it with sudo setsebool -P httpd_can_network_connect 1. sudo ausearch -m avc -ts recent confirms the denial first.

Cause 4: the backend crashed or was killed mid-request

prematurely closed and Connection reset by peer mean the backend accepted the request and then disappeared. Three things do that:

  • The kernel’s OOM killer. sudo journalctl -k | grep -i 'killed process' shows it. PHP-FPM logs the loss of a worker as child 12345 exited on signal 9 (SIGKILL). The OOM-killed process guide finds what used the memory.
  • A crash. exited on signal 11 (SIGSEGV) in the FPM log points at a PHP extension; an unhandled exception in Node ends the process unless something catches it, and systemd restarts it a moment later.
  • The backend’s own timeout. gunicorn kills a worker after 30 seconds by default, and PHP-FPM’s request_terminate_timeout logs execution timed out ... terminating. nginx sees a closed connection and answers 502 rather than 504. That is a slow request, covered in nginx 504 Gateway Timeout.

Cause 5: the response headers are too big

nginx reads the backend’s response headers into one buffer, 4 or 8 KB by default (one memory page). A login that sets several large cookies, or a framework that sends a long Content-Security-Policy or Link header, overflows it, and the log says upstream sent too big header. Raise the buffer and the buffers together, or nginx -t fails with a proxy_busy_buffers_size error:

nginx · server or location block
# proxy_pass backends
proxy_buffer_size 16k;
proxy_buffers 8 16k;

# PHP-FPM (fastcgi_pass)
fastcgi_buffer_size 16k;
fastcgi_buffers 8 16k;

Then sudo nginx -t && sudo systemctl reload nginx. If the headers keep growing, find out why: a cookie that grows with every visit is a bug, and Cloudflare refuses response headers over its own limit with a 520.

Cause 6: a firewall or keep-alive between proxy and backend

  • A backend on another server. A firewall that rejects the connection gives (111: Connection refused) or (113: No route to host) and a 502; one that drops packets gives a timeout and a 504. Allow the proxy’s address on the backend’s port, for example with ufw, and test with curl -v --connect-timeout 5 http://10.0.0.5:8080/ from the proxy.
  • Rare, random 502s behind a load balancer. When the backend closes idle keep-alive connections sooner than the balancer stops reusing them, a request occasionally lands on a connection that is just closing. Node’s server.keepAliveTimeout defaults to 5 seconds, shorter than the idle timeout of most balancers. Set the backend’s keep-alive longer than the balancer’s.

502 vs 503 vs 504

Question502 Bad Gateway503 Service Unavailable504 Gateway Timeout
Who sends itA proxy or gatewayThe server itself, or a proxyA proxy or gateway
What happenedThe backend's answer was invalid, cut off or impossible to getThe server declines for now: overload, maintenance, a rate limitThe backend accepted the request but did not answer in time
Typical nginx logconnect() failed (111)limiting requestsupstream timed out (110)
Retry-After headerNoOften, for planned maintenanceNo
First checkIs the backend running, and listening where the proxy expects?Worker pool, maintenance flag, rate limitsSlow queries, slow outside calls, timeouts

503 Service Unavailable and 504 Gateway Timeout have their own guides. A 500 Internal Server Error is the other neighbour: there the backend did answer, with an error of its own.

Once the backend answers again, the HTTP header checker requests the page from outside your network and shows the status code and headers the world gets.

Keeping 502s away on your own server

  • Let systemd restart a crashed app. Restart=on-failure in the unit turns a crash into a few seconds of 502 instead of an outage; the systemd service guide shows the unit.
  • Reload nginx instead of restarting it. systemctl reload nginx starts new workers and lets the old ones finish their requests; a restart drops them.
  • Leave memory headroom. Size pm.max_children and app workers to the RAM you have, and add swap as a buffer, so a busy hour does not end with the OOM killer. A Standard KVM plan with more memory is the cure when the workload has simply outgrown the server.
  • Watch it from outside. A monitor such as Uptime Kuma notices a 502 before your visitors write to you.

Frequently asked

What does 502 Bad Gateway mean?

A server acting as a gateway or proxy, such as nginx, Apache, a load balancer or a CDN, got an invalid response, or no response at all, from the server behind it. The proxy itself works; the application or server it forwards to is down, unreachable, or broke off its answer.

Is a 502 Bad Gateway my fault as a visitor?

Almost never. It happens between two servers, so clearing cookies or the browser cache rarely helps. Wait a minute and reload once. If every site shows 502, a VPN, corporate proxy or security product on your side is the gateway that fails.

How do I fix a 502 Bad Gateway on nginx?

Read /var/log/nginx/error.log. Connection refused means the backend is not running or listens elsewhere; No such file or directory on a .sock means a wrong socket path, often after a PHP upgrade; Permission denied means socket ownership or SELinux; prematurely closed means the backend crashed or was killed mid-request. Fix that cause, then reload nginx.

What is the difference between 502 and 504?

Both come from a proxy. 502 means the backend's answer was invalid or could not be obtained at all, for example a refused connection or a crash mid-request. 504 means the backend accepted the request but did not answer before the proxy's timeout, 60 seconds by default in nginx.

Related articles

All troubleshooting