Skip to content
Control Plane Labs

502 Bad Gateway

Server error response defined by RFC 9110 §15.6.3.

Last updated July 27, 2026

Common causes at a glance

  • Upstream process not running, so the proxy gets connection refused
  • Upstream crashed or was OOM-killed part-way through the response
  • Proxy speaking cleartext to a TLS port, or TLS to a cleartext port
  • Response headers larger than the proxy buffer, such as an oversized Set-Cookie

Reproduce it

curl -sS -o /dev/null -w 'code=%{http_code} t=%{time_total}\n' https://www.example.com/api/health

What 502 Bad Gateway means

A 502 is always generated by an intermediary, which is why the page looks nothing like your application’s error output. Three distinct situations collapse into this one code, and they have different fixes.

Connection refused: nothing is listening on the upstream address. The process crashed, the container was rescheduled, the port in the proxy config does not match the port the app binds. Premature close: something accepted the request and then died before finishing the response — an out-of-memory kill, a worker timeout, a panic. Invalid response: the upstream is alive and speaking, but not HTTP. That is a cleartext service behind a proxy configured for TLS, a TLS service behind a proxy speaking cleartext, or response headers exceeding the proxy’s buffer.

What the spec says

RFC 9110 §15.6.3 is narrow: the server, while acting as a gateway or proxy, received an invalid response from an inbound server it accessed. The word doing the work is invalid. A merely slow upstream is 504, and an upstream that deliberately returns 500 is passed through as 500. The spec gives no guidance on retries, so proxy behaviour is implementation-defined: nginx decides via proxy_next_upstream, which by default retries only error and timeout conditions, and only for requests it considers safe to repeat.

What actually causes it

The proxy error log tells you which of the three it was, and the phrasing is worth learning. nginx writes connect() failed (111: Connection refused) while connecting to upstream for a dead listener, upstream prematurely closed connection while reading response header for a mid-response death, and upstream sent invalid header or no live upstreams for garbage and for every backend being marked down. A 502 only under load usually means the upstream is being killed by a memory limit. A 502 constant from the first request after a deploy is almost always a port, scheme, or hostname mismatch in the proxy configuration.

How to debug it

Take the proxy out of the picture. Hit the upstream on its own address and see whether it answers HTTP at all:

curl -sS -D - -o /dev/null --max-time 5 http://10.0.1.24:8080/api/health

If that works and the public URL 502s, the problem is the proxy’s view of the upstream: wrong port, wrong scheme, DNS resolving to a stale pod IP, or health checks having ejected every backend. Check proxy_pass and proxy_buffer_size and the pool state in the upstream module. If the direct request also fails, read that service’s logs around the timestamp for a crash.

Working examples

curl

# 1. Public URL: confirm the 502 and note how fast it arrives.
curl -sS -o /dev/null -w 'code=%{http_code} t=%{time_total}\n' \
  https://www.example.com/api/health

# 2. Straight at the upstream, bypassing the proxy entirely.
curl -sS -D - -o /dev/null --max-time 5 http://10.0.1.24:8080/api/health

# 3. Is anything even listening? A refused connection makes curl exit 7.
curl -sS --max-time 3 http://10.0.1.24:8080/ ; echo "exit=$?"

Python (requests)

import time
import requests

# A fast 502 usually means connection refused; a slow one means the upstream
# accepted the request and then died. Time it.
start = time.monotonic()
resp = requests.get("https://www.example.com/api/health", timeout=15)
print(resp.status_code, f"{time.monotonic() - start:.2f}s", resp.headers.get("Server"))

try:
    direct = requests.get("http://10.0.1.24:8080/api/health", timeout=5)
    print("upstream:", direct.status_code)
except requests.exceptions.ConnectionError as err:
    print("upstream refused or reset:", err)

Node (fetch)

// Distinguish "proxy answered 502" from "upstream unreachable from here".
const t0 = performance.now();
const res = await fetch("https://www.example.com/api/health");
console.log(res.status, ((performance.now() - t0) / 1000).toFixed(2) + "s",
  res.headers.get("server"));

try {
  const direct = await fetch("http://10.0.1.24:8080/api/health",
    { signal: AbortSignal.timeout(5000) });
  console.log("upstream:", direct.status);
} catch (err) {
  console.error("upstream unreachable:", err.message); // ECONNREFUSED, timeout, ...
}

Reference and tooling

The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.

Other 5xx codes