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

Source: https://howhttpworks.com/compare/502-vs-503-vs-504
Last reviewed: 2026-10-04

> **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 Gateway | 503 Service Unavailable | 504 Gateway Timeout |
|---|---|---|---|
| Who generates it | A proxy, gateway or CDN | The origin, or a proxy/LB acting on policy | A proxy, gateway or CDN |
| What the upstream did | Refused, reset, closed early or sent an invalid response | Nothing wrong with the wire; it (or the LB) chose to refuse | Accepted the connection but did not answer in time |
| Spec | RFC 9110 15.6.3 | RFC 9110 15.6.4 | RFC 9110 15.6.5 |
| `Retry-After` expected | No | Optional, often useful | No |
| Usual root cause | App crashed, wrong port or socket, keep-alive race, oversized headers | Overload, deploy drain, no registered targets, rate or connection limit | Slow query, blocked worker, timeout shorter than the work |
| nginx error log | `connect() 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 cousins | 502, 520, 521, 523 | 503 (from your origin or Cloudflare) | 504, 522, 524 |
| Detail pages | [502](https://howhttpworks.com/status-codes/502), [nginx 502 fix](https://howhttpworks.com/debug/nginx-502-bad-gateway) | [503](https://howhttpworks.com/status-codes/503) | [504](https://howhttpworks.com/status-codes/504), [nginx 504 fix](https://howhttpworks.com/debug/nginx-504-gateway-timeout) |

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

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

Things to look at in the headers and body:

- `Server: nginx` with a plain centered "502 Bad Gateway" body: nginx generated it. Your app may not have been reached.
- `Server: cloudflare` plus a `CF-Ray` header, and a branded error page with a numeric code (520-526): Cloudflare generated it, and the code tells you what happened between Cloudflare and your origin.
- `Server: awselb/2.0`: an AWS Application Load Balancer. Its 502, 503 and 504 appear in the ALB access log with `elb_status_code` set and `target_status_code` set to `-` when the target never responded.
- Your framework's own error format (JSON from your API, a Django or Rails page): the application produced it. Check the app log, not the proxy.

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

```text
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**.

```text
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**.

```text
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.

```text
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 happened | nginx says | Cloudflare shows | AWS ALB |
|---|---|---|---|
| Origin refused the connection | 502, `Connection refused` | 521 Web Server Is Down | 502 |
| Origin closed the connection or replied with garbage | 502, `prematurely closed` | 520 Web Server Returned an Unknown Error | 502 |
| TCP connect to origin timed out | 504, `while connecting to upstream` | 522 Connection Timed Out | 504 |
| Origin took too long to respond | 504, `while reading response header` | 524 A Timeout Occurred (100 s by default on non-Enterprise) | 504 once the idle timeout (60 s default) expires |
| No route to origin | n/a | 523 Origin Is Unreachable | n/a |
| TLS failure toward origin | 502, `SSL_do_handshake() failed` | 525 SSL Handshake Failed, 526 Invalid SSL Certificate | 502 |
| Deliberate shedding or maintenance | 503 | 503 passed through from origin | 503 when the target group has no registered targets |

Per-code pages: [520](https://howhttpworks.com/status-codes/520), [521](https://howhttpworks.com/status-codes/521), [522](https://howhttpworks.com/status-codes/522), [523](https://howhttpworks.com/status-codes/523), [524](https://howhttpworks.com/status-codes/524), [525](https://howhttpworks.com/status-codes/525), [526](https://howhttpworks.com/status-codes/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
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
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](https://howhttpworks.com/status-codes/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

- [MDN Web Docs: 502 Bad Gateway](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502)
- [MDN Web Docs: 503 Service Unavailable](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503)
- [MDN Web Docs: 504 Gateway Timeout](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504)
- [RFC 9110: 502 Bad Gateway (section 15.6.3)](https://www.rfc-editor.org/rfc/rfc9110#section-15.6.3)
- [RFC 9110: 503 Service Unavailable (section 15.6.4)](https://www.rfc-editor.org/rfc/rfc9110#section-15.6.4)
- [RFC 9110: 504 Gateway Timeout (section 15.6.5)](https://www.rfc-editor.org/rfc/rfc9110#section-15.6.5)
- [nginx: ngx_http_proxy_module](https://nginx.org/en/docs/http/ngx_http_proxy_module.html)
- [Cloudflare: Troubleshooting 5xx errors](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/)
