502 Bad Gateway
Server error response defined by RFC 9110 §15.6.3.
Written and maintained by Ben Ennis
Last reviewed July 27, 2026 · How we verify this
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/healthWhat 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.
Cloudflare doesn’t send a plain 502 — it splits the failure into eight codes
The three-way split above (refused, premature close, invalid response) is exactly the kind of ambiguity Cloudflare’s proxy resolves before the browser ever sees a status. Instead of a generic 502, Cloudflare’s edge returns one of several branded 5xx error codes depending on exactly what happened talking to your origin: 521 means the origin refused the TCP connection outright — the connection-refused case from above — while 522 means the connection timed out during setup, 523 means the origin’s address is unreachable (DNS or routing, not the server itself), and 525 or 526 mean the TLS handshake failed or the origin’s certificate is invalid, a failure mode this page’s generic “invalid response” bucket does not distinguish from a plaintext/TLS scheme mismatch at all.
The practical benefit is that Cloudflare’s code tells you which side of the retry logic to look at without reading a raw proxy log. A 521 means look at whether the origin process is running and whether Cloudflare’s IP ranges are allow-listed in your firewall, since a security rule blocking Cloudflare’s own IPs is a common self-inflicted cause of “the server is up but refuses us specifically.” A 522 or 524 means the process is reachable but too slow, which is a capacity or timeout-budget problem rather than a crash. If your stack sits behind Cloudflare, treat the specific 5xx code as the diagnosis and reserve the generic three-cause 502 investigation on this page for whatever proxy layer sits between Cloudflare and your application, since Cloudflare has already done the harder part of narrowing it down.
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.
Related reading
- SLOs, SLIs, and error budgets: a primer
This is the kind of error you should be measuring against an error budget rather than paging on individually. How to pick an indicator and alert on burn rate.
- Karpenter vs cluster-autoscaler in 2026
On Kubernetes this status often tracks node churn rather than application failure. How the two autoscalers differ now that they share an API.
- Every HTTP status code, and when it shows up
The wider map: which codes you actually meet in production, and the pairs everyone confuses.
Other 5xx codes
- 500 Internal Server Error
- 501 Not Implemented
- 503 Service Unavailable
- 504 Gateway Timeout
- 505 HTTP Version Not Supported
- 506 Variant Also Negotiates
- 507 Insufficient Storage
- 508 Loop Detected
- 510 Not Extended
- 511 Network Authentication Required
- 520 Web Server Returns an Unknown Error
- 521 Web Server Is Down
- 522 Connection Timed Out
- 523 Origin Is Unreachable
- 524 A Timeout Occurred
- 525 SSL Handshake Failed
- 526 Invalid SSL Certificate
- 530 Unable to Resolve Origin Hostname