Blog
A 502 means your proxy could not get a valid response from the upstream. Here is how to find which cause you have, with log lines, commands and monitoring checks.
A 502 Bad Gateway means a reverse proxy, load balancer or CDN reached out to the application behind it and got no valid response: the connection was refused, dropped mid-request or answered with something the proxy could not parse. The most common 502 bad gateway causes are a crashed or stopped upstream process, a wrong port or socket in the proxy config, keepalive and timeout mismatches, exhausted PHP-FPM or Node workers, oversized response headers and the gap during deploys. Each one leaves a distinct line in the proxy's error log, which is where debugging should start.
Every 502 involves two parties: the proxy and upstream. The proxy (nginx, HAProxy, a cloud load balancer, a CDN edge) accepts the client's request and forwards it to the upstream (your Node app, PHP-FPM, Gunicorn, a container). The 502 is generated by the proxy, not by your application. Your app usually never sees the request that failed, which is why its own logs often look clean during an outage.
It helps to separate the 5xx codes a proxy can emit:
In practice the lines blur. Some load balancers return 502 when an upstream resets a connection that a different product would report as 504. That is another reason to read the error log instead of guessing from the status code alone.
Modern stacks often chain several proxies: CDN, cloud load balancer, nginx, then the app. Before debugging any single cause, find out which hop generated the error. Response headers and the error page body usually give it away.
curl -sv -o /dev/null https://example.com/api/health 2>&1 | grep -iE '^< (HTTP|server|via|x-cache|cf-|x-amz)'If the Server header says nginx and the body is nginx's default error page, your own nginx produced it. If the headers belong to your CDN or load balancer, the problem sits between that layer and your origin, which may itself be fine. Then bypass each layer in turn: request the origin directly by IP with the right Host header, then the upstream port from the proxy host itself.
# from the proxy host, skip nginx and hit the app directly
curl -sv http://127.0.0.1:3000/health
# hit the origin while skipping the CDN
curl -sv --resolve example.com:443:203.0.113.10 https://example.com/healthIf the upstream answers fine when called directly, the issue is in the proxy configuration or the connection between them. If it does not answer, you have a dead or overloaded upstream.
The proxy's error log names the failure mode in one line, so read it before restarting anything, because a restart often destroys the evidence. For nginx that is usually /var/log/nginx/error.log. The sections below map the typical log lines to causes.
Log line: connect() failed (111: Connection refused) while connecting to upstream. Nothing is listening on the target address. The process crashed, was never started after a reboot, or was killed.
systemctl status myapp
journalctl -u myapp --since "15 min ago"
# was it killed by the kernel for running out of memory?
journalctl -k | grep -i -E 'out of memory|killed process'The OOM killer is a frequent silent culprit: the app grows, the kernel kills it, the supervisor restarts it, and in between every request is a 502. If restarts are frequent, fix the memory growth or the limit rather than adding faster restarts.
Also produces Connection refused, or for Unix sockets (2: No such file or directory) and (13: Permission denied). Typical triggers: the app's port changed in an environment variable but not in the proxy config, PHP was upgraded and the socket path moved from php8.1-fpm.sock to php8.2-fpm.sock, or a container listens on 127.0.0.1 inside its own network namespace and is unreachable from outside.
# what is actually listening, and on which address?
ss -ltnp | grep -E ':3000|:8080'
ls -l /run/php/
# compare with what nginx expects
nginx -T | grep -E 'proxy_pass|fastcgi_pass|server .*:'For permission errors, the socket owner and mode set by listen.owner, listen.group and listen.mode in the PHP-FPM pool must allow the nginx worker user to connect. For containers, the app must bind to 0.0.0.0, not localhost. A separate check on the upstream port is a cheap way to confirm this layer, and our guide on combining TCP port checks with HTTP checks explains what such a check proves and what it does not.
Log line: upstream prematurely closed connection while reading response header from upstream. The upstream accepted the connection, then dropped it before sending a complete response. Common reasons:
request_terminate_timeout or by a crash in a native extension.The keepalive case produces sporadic 502s that are hard to reproduce, often after quiet periods. Node.js, for instance, closes idle keepalive connections after 5 seconds by default, while many load balancers keep idle connections open much longer. The rule is simple: the upstream's idle timeout must be longer than the proxy's.
// Node.js: keep idle sockets open longer than the load balancer does
const server = app.listen(3000);
server.keepAliveTimeout = 65000; // ms, above a 60 s proxy idle timeout
server.headersTimeout = 66000; // must be higher than keepAliveTimeoutCheck your load balancer's configured idle timeout and set the upstream a few seconds above it.
When every worker is busy, new requests queue. Once the queue is full, the proxy cannot connect and returns 502. With PHP-FPM, the FPM log shows the warning first:
WARNING: [pool www] server reached pm.max_children setting (10), consider raising itand nginx then logs (11: Resource temporarily unavailable) while connecting to upstream when the socket backlog overflows. Raising pm.max_children only helps if there is memory for it. Estimate it from the average worker size:
# average resident memory per php-fpm worker in MB
ps -C php-fpm8.2 -o rss= | awk '{s+=$1; n++} END {print s/n/1024 " MB"}'Divide the memory you can spare for PHP by that number to get a ceiling. If workers are busy because requests are slow (database locks, external API calls), more workers just move the bottleneck. Enable the FPM slowlog with request_slowlog_timeout = 5s to see which code paths hold workers.
Node has the same problem in a different shape: a single process with a blocked event loop stops accepting connections, and a process that hits its heap limit crashes with JavaScript heap out of memory, followed by refused connections until the supervisor restarts it. Run several processes behind the proxy and watch event loop lag, not just CPU.
Log line: upstream sent too big header while reading response header from upstream. Large cookies, long redirect URLs or verbose debug headers exceed nginx's default header buffer. The app returned a perfectly valid response, but nginx refuses to pass it through.
location / {
proxy_pass http://app;
proxy_buffer_size 16k;
proxy_buffers 8 16k;
}
# for PHP-FPM use fastcgi_buffer_size and fastcgi_buffers insteadRaise the buffers moderately, then find out why the headers are so large. A session cookie that grows without bound will eventually break any limit.
If 502s cluster around release times, the deploy window is the cause: the old process stops before the new one is ready to accept connections, or the new one is listening but still warming up caches and connection pools. The log shows Connection refused for a few seconds, then everything recovers.
Fixes, from simplest to most robust:
systemctl reload, PHP-FPM's reload, a process manager's zero-downtime reload) instead of hard restarts.proxy_next_upstream error timeout http_502;.tail -n 50 /var/log/nginx/error.log.systemctl status and journalctl -k.pm.max_children, event loop lag or process count for Node.A 502 is easy to miss with the wrong monitor. A check that only confirms the proxy port is open passes happily while every request returns 502, because nginx itself is fine. Design checks around the path a real request takes.
# count 502s in the last 10,000 requests (combined log format, status in field 9)
tail -n 10000 /var/log/nginx/access.log | awk '$9 == 502 {c++} END {print c+0}'Feed a count like this into your metrics system and alert when it stays above zero for several minutes. Synthetic checks catch hard outages, log-based alerts catch the partial ones.
If you would rather not build this yourself, m0nitor is one option. Its uptime checks for websites, APIs and servers run on a fixed interval from distributed probes in several regions, so a 502 on your health endpoint is detected without waiting for a user report. Alerts can go to email, Slack, Microsoft Teams, PagerDuty and other channels, and alert workflows can react to a failed check by notifying a channel, creating an incident or calling a webhook, for example one that collects the proxy's error log before anyone restarts the service.
Almost always the server side. The 502 is generated by a proxy, load balancer or CDN that could not get a valid response from the upstream application. Clearing the browser cache rarely helps, unless a local proxy or VPN on the client's network is the gateway that fails.
A 502 means the upstream refused the connection, dropped it or returned an invalid response. A 504 means the upstream accepted the connection but did not answer before the proxy's timeout expired. Slow queries usually produce 504s, crashed or saturated processes usually produce 502s.
Intermittent 502s typically come from keepalive timeout mismatches between proxy and upstream, worker pools that saturate only under peak load, processes restarted by the OOM killer, or deploys. Compare the timestamps in the proxy's error log with traffic graphs, deploy history and kernel logs to see which pattern matches.
Yes. The CDN edge may fail to reach your origin because of firewall rules blocking its IP ranges, TLS mismatches between edge and origin, or an origin address that changed. Request the origin directly while bypassing the CDN: if that works, the problem lies in the connection between CDN and origin.