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.
# 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 see | Sent by | What it usually means |
|---|---|---|
White page, 502 Bad Gateway, footer nginx | nginx | Backend 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_proxy | The backend's answer broke off or was not HTTP (AH01102) |
| 502 Bad Gateway. The server returned an invalid or incomplete response. | HAProxy | The server in the backend sent nothing usable |
| Empty page with status 502 | Caddy | Its log names the dial error, such as connect: connection refused |
Bad Gateway | Traefik | No answer from the service or container |
HTTP Error 502.3 or 502.5 Process Failure | IIS (ARR or the ASP.NET Core module) | The backend failed, timed out, or the app did not start |
| Cloudflare-branded 502 page | Cloudflare's own network | A problem inside Cloudflare; check its status page |
| Plain 502 page while the site runs behind Cloudflare | Your origin's proxy, passed through | Everything 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:
| Code | What Cloudflare saw |
|---|---|
| 520 | The origin answered with something empty, reset or malformed |
| 521 | The origin refused the connection: web server stopped or port closed |
| 522 | The TCP connection to the origin timed out |
| 523 | The origin is unreachable: wrong IP, routing or DNS |
| 524 | Connected, but no HTTP response in time |
| 525 | The TLS handshake with the origin failed |
| 526 | The 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 message | Cause | Fix |
|---|---|---|
connect() failed (111: Connection refused) while connecting to upstream | Nothing listens on that address and port: the app is down, crashed, or listens elsewhere | Start 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 stopped | Fix the path |
(13: Permission denied) while connecting to upstream | nginx may not open the socket, or SELinux blocks the connection | Socket owner or SELinux boolean |
upstream prematurely closed connection while reading response header from upstream | The backend died or killed the request before it answered | Crash, OOM kill or worker timeout |
recv() failed (104: Connection reset by peer) while reading response header from upstream | The same family: the connection was reset mid-request | Check the backend’s log |
upstream sent too big header while reading response header from upstream | Response headers (usually cookies) exceed the proxy buffer | Raise the buffer size |
no live upstreams while connecting to upstream | Every server in the upstream block was marked failed | Fix 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:
# 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 5If 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:
curl: (7) Failed to connect to localhost port 80 after 0 ms: Couldn't connect to serverPHP-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:
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: pongAn 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:
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 farActive: 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.sockbecomesphp8.3-fpm.sock, andfastcgi_passstill names the old one.ls /run/php/shows what exists. - localhost is not one address.
proxy_pass http://localhost:3000resolves to both::1and127.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 to127.0.0.1while a Node app (version 17 or later) bound tolocalhostlistens only on::1. Theupstream: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.1and proxy to that, or put the proxy in the same Docker network and use the service name.
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:
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = nginx ; the group of the nginx worker user
listen.mode = 0660Restart 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 aschild 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_timeoutlogsexecution 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:
# 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 withcurl -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.keepAliveTimeoutdefaults 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
| Question | 502 Bad Gateway | 503 Service Unavailable | 504 Gateway Timeout |
|---|---|---|---|
| Who sends it | A proxy or gateway | The server itself, or a proxy | A proxy or gateway |
| What happened | The backend's answer was invalid, cut off or impossible to get | The server declines for now: overload, maintenance, a rate limit | The backend accepted the request but did not answer in time |
| Typical nginx log | connect() failed (111) | limiting requests | upstream timed out (110) |
| Retry-After header | No | Often, for planned maintenance | No |
| First check | Is the backend running, and listening where the proxy expects? | Worker pool, maintenance flag, rate limits | Slow 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-failurein 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 nginxstarts new workers and lets the old ones finish their requests; a restart drops them. - Leave memory headroom. Size
pm.max_childrenand 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.