Who is talking
A 4xx or 5xx from your own code means your application answered. 502, 503 and 504 are usually not that. They are generated by a proxy, a load balancer or a CDN that sits in front of the application and could not get a usable answer out of it. The body will often confirm this: nginx's plain "502 Bad Gateway" page, an AWS ALB's terse text, Cloudflare's branded error page with a Ray ID. Working out which layer produced the page is the first step, and the Server, Via and vendor headers in the response usually say. The HTTP header checker shows them exactly as returned.
The three, side by side
| Code | The proxy is saying | What is usually wrong |
|---|---|---|
| 502 Bad Gateway | "I reached the upstream, or tried to, and what I got back was not a valid response." | The application process is not listening (crashed, restarting, deploying), refused the connection, closed it mid-response, sent malformed HTTP, or the proxy could not complete a TLS handshake with it. |
| 503 Service Unavailable | "The service is deliberately not available right now." | The application, or the proxy on its behalf, is saying no on purpose: overload protection, maintenance mode, no healthy backends in the pool, a rate limit, a circuit breaker open. |
| 504 Gateway Timeout | "I reached the upstream and it never finished answering within my timeout." | The application is alive but slow: a request that takes longer than the proxy's read timeout, a database query that hangs, a downstream API call with no timeout of its own, a deadlock. |
502: a bad or missing answer
The proxy got as far as trying to talk to the upstream and the conversation failed. In nginx terms this is connect() failed (111: Connection refused), upstream prematurely closed connection, or upstream sent invalid header, all in the error log with the upstream address. The most common cause by a distance is a deploy: the old process has stopped and the new one is not yet listening, so for a few seconds every request is refused. Rolling deploys and a health-checked pool fix this; a single instance restarted in place produces a burst of 502s every release.
Other causes worth checking, roughly in order: the application crashed (look for the process, look for an out-of-memory kill), the proxy is pointing at the wrong port or address after a config change, a TLS mismatch between proxy and upstream (proxy expects HTTPS, upstream serves HTTP, or the upstream certificate is not trusted by the proxy), and a response with headers too large for the proxy's buffer (nginx's upstream sent too big header, which is a 502 and needs proxy_buffer_size raised).
One more that catches people: an upstream that returned a redirect the proxy then tried to follow and could not. Proxies do not normally follow redirects, but a serverless function or an edge worker that fetches your origin will, and a 3xx with no Location becomes a 502 in the worker. You can reproduce that shape with the HTTP status tester, which returns any code you ask for.
503: unavailable on purpose
503 is the one of the three that the application itself often sends. It means "not now": a maintenance page, a load shedder rejecting requests over a concurrency limit, a dependency that is known to be down. It is also what a load balancer sends when every backend in its pool has failed its health check, which is a different problem from 502 even though the root cause (the app is down) may be the same: with 503 from the balancer the app has been down long enough for health checks to notice and remove it, so look at why the health checks fail rather than at a single request.
503 should carry a Retry-After header when the outage is planned, and clients that see one should honour it. It is the correct code for maintenance windows precisely because search engines treat it as temporary and come back, where a 500 or a 404 on every page for an hour can cost you rankings.
504: alive but too slow
The connection to the upstream worked and the request was sent; the response did not arrive before the proxy's timeout. nginx logs upstream timed out (110: Connection timed out) while reading response header from upstream. The timeout is the proxy's (nginx proxy_read_timeout defaults to 60 seconds; ALB idle timeout is 60; Cloudflare's origin timeout is 100 and cannot be changed on most plans), and the application may still be working away on the request after the visitor has been given an error, which is how a slow endpoint under load turns into a pile-up.
The fix is almost never to raise the proxy timeout. It is to find what the application is waiting on: a query without an index, an outbound HTTP call with no timeout, a lock, a thread pool that is exhausted. Set a timeout inside the application that is shorter than the proxy's, so the application fails fast with a 500 or 503 it controls rather than being cut off. Long-running work belongs in a job with a polling endpoint, not in a request.
Telling them apart from the outside
- Fetch the failing URL with the HTTP checker and look at the timing. A 502 comes back fast (the connection was refused immediately). A 504 comes back after a suspiciously round number of seconds: 30, 60, 100. A 503 can be either.
- Read the headers and body. Which product generated the page? If it is the CDN, the problem is between CDN and origin; if it is nginx on your origin, it is between nginx and the app.
- Check whether HTTPS to the origin works at all, if you can reach it directly. A CDN reporting 502 with an origin that serves a bad certificate is a TLS mismatch, not an application fault.
- Then go to the proxy's error log, which names the upstream and the reason in one line.
Which one your own code should send
- Overloaded or shedding load: 503, with
Retry-After. - Planned maintenance: 503, with
Retry-After. - A dependency you call timed out: 504 if you are acting as a gateway to it, 503 if you are the service and just cannot serve right now. Either is defensible; be consistent.
- You received garbage from something you proxy to: 502.
- Your own code threw: 500. Do not dress a bug up as a gateway error.
For testing how your clients, retries and monitors react to each, the HTTP status code tester returns any of them on demand, with an optional delay for reproducing a 504's timing.