Blog

502 Bad Gateway causes: what fails between proxy and upstream, how to debug each one and how to get alerted first

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.

What a 502 Bad Gateway actually means

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:

  • 502: the upstream was unreachable or returned an invalid response.
  • 503: the service is deliberately unavailable, for example no healthy backends or a maintenance mode.
  • 504: the upstream accepted the connection but did not answer within the proxy's timeout.

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.

Which layer is returning the 502?

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/health

If 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 common 502 bad gateway causes and how to debug each

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.

1. The upstream process crashed or is not running

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.

2. Wrong port, address or socket

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.

3. The upstream closed the connection early

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:

  • The worker was killed mid-request, for example by PHP-FPM's request_terminate_timeout or by a crash in a native extension.
  • A keepalive timeout mismatch: the proxy reuses an idle connection that the upstream has just closed.

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 keepAliveTimeout

Check your load balancer's configured idle timeout and set the upstream a few seconds above it.

4. PHP-FPM or Node process limits are exhausted

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 it

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

5. The upstream sent an invalid or oversized response

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 instead

Raise the buffers moderately, then find out why the headers are so large. A session cookie that grows without bound will eventually break any limit.

6. Deploys and restarts

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:

  • Use graceful reloads (systemctl reload, PHP-FPM's reload, a process manager's zero-downtime reload) instead of hard restarts.
  • Run at least two upstream instances and restart them one at a time.
  • Let nginx retry another instance on connection errors with proxy_next_upstream error timeout http_502;.
  • Only route traffic to a new instance after its readiness endpoint returns 200.

A five-minute triage checklist

  1. Confirm which layer returns the 502 by checking response headers and the error page.
  2. Read the last lines of the proxy's error log: tail -n 50 /var/log/nginx/error.log.
  3. Call the upstream directly from the proxy host to separate proxy from app.
  4. Check whether the process is running and whether the kernel killed it: systemctl status and journalctl -k.
  5. Look at worker saturation: the FPM log for pm.max_children, event loop lag or process count for Node.
  6. Correlate the start time with deploys, config changes and traffic spikes.

How to get alerted about 502s before users report them

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.

  • Check an endpoint that reaches the upstream. A static file or the homepage served from a cache proves nothing about the app. Use a health endpoint that executes application code and touches its critical dependencies.
  • Assert the status code. Treat any non-2xx as a failure, and optionally assert a keyword in the body so an error page with status 200 is caught too.
  • Check from several regions with a short confirmation. One retry or a second region filters network noise without hiding a real outage. Our guide to reducing false positive alerts covers retry counts and timeouts in detail.
  • Add an upstream port check. If the HTTP check fails and the upstream port is closed, you know immediately that the app is down rather than the proxy misconfigured.
  • Watch the 502 rate in your access logs. Intermittent 502s from keepalive mismatches or worker exhaustion may never coincide with a synthetic check, but they show up in logs.
# 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.

Monitoring 502s with m0nitor

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.

FAQ

Is a 502 Bad Gateway caused by the client or the server?

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.

What is the difference between a 502 and a 504?

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.

Why do 502 errors happen only intermittently?

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.

Can a CDN cause a 502 even if my server is fine?

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.

Product

  • Pricing
  • Features
  • Integrations
  • Security
  • Free network check

Features

  • Uptime monitoring
  • Status pages
  • Alert workflows
  • Incident management
  • Dependencies
  • AI assistant
  • Global infrastructure
  • On-call schedules
  • Enterprise SSO

Use cases

  • Websites for free
  • Cron jobs
  • SSL certificates
  • Databases
  • IP blacklists
  • GDPR compliance

Resources

  • What is a status page?
  • Documentation
  • API
  • MCP server
  • Monitoring glossary
  • Free tools
  • SLA calculator
  • SSL checker
  • DNS lookup
  • Blog
  • Changelog
  • About
  • Contact

Compare

  • Free uptime monitoring
  • UptimeRobot
  • Better Stack
  • Pingdom
  • StatusCake
  • Uptime Kuma
  • Hyperping
  • Atlassian Statuspage
  • Instatus
  • Cachet
  • Freshping