How HTTP Works

Comparison

502 vs 503 vs 504: Which Gateway Error Is It

502, 503 and 504 side by side: who generated each one, what the upstream did, the nginx error-log line that proves it, and the Cloudflare 52x and ALB equivalents.

Bottom line: 502 means the upstream answered with garbage or hung up. 504 means it never answered in time. 503 means a server deliberately said it is overloaded or in maintenance. The access log and error log tell you which.

By How HTTP WorksReview process
502 Bad Gateway
vs
504 Gateway Timeout

TL;DR: 502 means the upstream sent something unusable or hung up on the proxy. 504 means the upstream never answered before the proxy’s timer ran out. 503 means a server deliberately said “not now” because of overload, maintenance or a limit. Read the proxy’s error log line: it names which of the three happened and which upstream did it.

The Three In One Table

502 Bad Gateway503 Service Unavailable504 Gateway Timeout
Who generates itA proxy, gateway or CDNThe origin, or a proxy/LB acting on policyA proxy, gateway or CDN
What the upstream didRefused, reset, closed early or sent an invalid responseNothing wrong with the wire; it (or the LB) chose to refuseAccepted the connection but did not answer in time
SpecRFC 9110 15.6.3RFC 9110 15.6.4RFC 9110 15.6.5
Retry-After expectedNoOptional, often usefulNo
Usual root causeApp crashed, wrong port or socket, keep-alive race, oversized headersOverload, deploy drain, no registered targets, rate or connection limitSlow query, blocked worker, timeout shorter than the work
nginx error logconnect() failed (111: Connection refused), upstream prematurely closed connection, recv() failed (104: Connection reset by peer)limiting requests, excess: ... (limit_req) or limiting connections by zone (limit_conn)upstream timed out (110: Connection timed out) while reading response header from upstream
Cloudflare cousins502, 520, 521, 523503 (from your origin or Cloudflare)504, 522, 524
Detail pages502, nginx 502 fix503504, nginx 504 fix

Step 1: Who Sent It

Before reading any log, work out which layer produced the status. A 5xx from your app and a 5xx from the proxy in front of it look identical in a browser.

curl -sS -D - -o /dev/null https://example.com/api/slow

Things to look at in the headers and body:

Step 2: What The Upstream Did

nginx writes one error-log line per failure with the upstream address, and that line almost always settles it.

2026/10/04 09:12:31 [error] 2841#2841: *9087 connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.9, server: example.com, request: "GET /api/orders HTTP/1.1", upstream: "http://127.0.0.1:3000/api/orders", host: "example.com"

connect() failed (111: Connection refused) means nothing is listening on that port. The app is down, crashed or bound to a different address: 502.

2026/10/04 09:14:02 [error] 2841#2841: *9102 upstream prematurely closed connection while reading response header from upstream, upstream: "http://127.0.0.1:3000/api/orders"

The upstream accepted the request and closed the socket without a response. A worker was killed (OOM, max_requests recycling) or a keep-alive connection was reused after the app closed it: 502.

2026/10/04 09:15:47 [error] 2841#2841: *9130 upstream timed out (110: Connection timed out) while reading response header from upstream, upstream: "http://127.0.0.1:3000/api/report"

The upstream connected but sent no response header within proxy_read_timeout, which defaults to 60 seconds: 504. If the line ends with while connecting to upstream, it hit proxy_connect_timeout (also 60s by default), meaning the host is unreachable or its accept queue is full.

2026/10/04 09:16:20 [error] 2841#2841: *9144 limiting requests, excess: 5.420 by zone "perip", client: 203.0.113.9

This is nginx deliberately rejecting traffic with limit_req, which answers 503 unless you set limit_req_status 429;. Not an upstream fault at all.

Step 3: Cloudflare And Load Balancer Equivalents

What happenednginx saysCloudflare showsAWS ALB
Origin refused the connection502, Connection refused521 Web Server Is Down502
Origin closed the connection or replied with garbage502, prematurely closed520 Web Server Returned an Unknown Error502
TCP connect to origin timed out504, while connecting to upstream522 Connection Timed Out504
Origin took too long to respond504, while reading response header524 A Timeout Occurred (100 s by default on non-Enterprise)504 once the idle timeout (60 s default) expires
No route to originn/a523 Origin Is Unreachablen/a
TLS failure toward origin502, SSL_do_handshake() failed525 SSL Handshake Failed, 526 Invalid SSL Certificate502
Deliberate shedding or maintenance503503 passed through from origin503 when the target group has no registered targets

Per-code pages: 520, 521, 522, 523, 524, 525, 526.

Which Should I Return From My Own Gateway

If you write a reverse proxy, API gateway or BFF, the spec guidance maps directly:

HTTP/1.1 502 Bad Gateway
Content-Type: application/json

{"error": "upstream_invalid_response", "upstream": "orders"}

Use 502 when you reached the upstream and its response was unusable or the connection failed. Use 504 when you waited and the deadline passed. Use 503 when you chose to refuse, for example a circuit breaker is open or a queue is full, and add Retry-After:

HTTP/1.1 503 Service Unavailable
Retry-After: 30
Content-Type: application/json

{"error": "overloaded"}

A plain application (not a gateway) that hit its own failure should usually return 500, not 502 or 504, because there was no upstream.

Common Mistakes

Raising proxy_read_timeout to fix a 504 without finding the slow call. The user still waits longer, and an ALB or Cloudflare in front has its own, lower limit. A 524 at 100 seconds ignores your nginx setting.

Reading a 502 as “the app is down.” Frequently the app is up and nginx is reusing an idle keep-alive connection the app already closed (app keep-alive shorter than the proxy’s), or the response headers exceeded proxy_buffer_size (upstream sent too big header while reading response header from upstream).

Letting health checks pass while real requests fail. A /health that returns 200 without touching the database keeps the instance in rotation while every real request hangs into a 504.

Retrying non-idempotent calls after a 504. The upstream often completed the write after the proxy gave up. Retrying a payment POST without an idempotency key double-charges.

Serving 503 for maintenance without Retry-After. Crawlers and clients then guess. Send the header, and for planned maintenance keep the window short so search engines treat it as temporary.

FAQ

What is the difference between 502 and 504?

Both are generated by a proxy or gateway, not by your application. 502 Bad Gateway means the proxy got an invalid response or the connection was refused or reset. 504 Gateway Timeout means the proxy gave up waiting for any response. In nginx the error log says “connect() failed (111: Connection refused)” or “upstream prematurely closed connection” for 502, and “upstream timed out (110: Connection timed out)” for 504.

Is a 503 the same as a 502?

No. RFC 9110 section 15.6.4 defines 503 as the server being currently unable to handle the request due to overload or maintenance, and it may carry a Retry-After header. A 503 is usually a deliberate answer (a load shedder, a rate limit in nginx, an ALB target group with no registered targets), whereas a 502 is the proxy reporting that the upstream misbehaved.

Why do I get 502 on one request and 504 on the next?

Usually one upstream instance is dying slowly. Requests hitting a crashed worker get a reset (502); requests landing on a wedged one hang until the proxy timeout (504). Check whether the failures correlate with a specific upstream IP in the error log, and with deploys, memory kills, or keep-alive timeouts shorter than the proxy idle timeout.

Which of these should clients retry?

All three are generally worth retrying with backoff for idempotent requests, and 503 may tell you how long to wait via Retry-After. Do not blindly retry a POST after a 504: the upstream may have finished the work after the proxy stopped waiting, so use an idempotency key.

What do Cloudflare 520, 521, 522, 523 and 524 correspond to?

They are Cloudflare-specific refinements of the same ideas. 520 (unknown or empty response) and 521 (origin refused the connection) map loosely to 502 territory; 522 (TCP connect timed out) and 524 (origin did not send a response within the proxy read timeout, 100 seconds by default on non-Enterprise plans) map to 504 territory; 523 means Cloudflare has no route to the origin IP.

How do I know whether the 502 came from my app or from the proxy?

Look at the response headers and body. A default nginx error page says “502 Bad Gateway” with “nginx” under it, a Cloudflare error shows a branded page with a Ray ID and a CF-Ray header, and an AWS ALB returns a bare-bones page with a Server: awselb/2.0 header. If your application framework would have rendered the error, you would see its own format, and your app logs would show the request.

References

Browse /search