502, 504 or 503? First work out which machine answered you

· 8 min read · HTTP status codes 502 incident response · 简体中文

“The site is down, it says 502” packs three different questions: which machine returned that number, whether your request ever reached the application, and whether hitting reload right now makes things worse. The answers differ, so the fixes differ. Below, a server on this machine emits each of these responses one at a time and the browser’s actual behaviour is recorded.

1. The four 5xx codes split on two sentences

  • 500 — the application threw. It knows who it is and it knows it failed.
  • 503 — the server is alive and is refusing work on purpose: deploying, saturated, queue full. Usually with a Retry-After.
  • 502 — the machine in front (nginx, ALB, a CDN edge, an API gateway) got something unusable back from what sits behind it: the process died, the connection was reset, the response was malformed.
  • 504 — the same front machine waited for the backend up to its own timeout, gave up, and answered on its behalf.

502 versus 504 is one sentence: the upstream answered with garbage, or it never answered in time. Neither number was written by your application — and “did the request get that far?” is precisely the expensive unknown in a 5xx investigation. The digit alone cannot tell you.

2. Measured: what a browser does with each of these

A local test server returned different results from different URLs, and this machine’s Chrome visited each one. Observed, not quoted from a spec:

What the server did What the browser showed What fetch() returned
502 with an HTML body The server’s own HTML, title and body rendered as sent status: 502, ok: false, 66 body characters readable, X-Served-By header present
502 with an empty body A blank page — but the URL bar still holds the http address and the document exists with 0 characters status: 502, 0 body characters
503 with Retry-After: 120 The server’s page status: 503, retry-after reads back "120"
429 with Retry-After: 60 Same status: 429, retry-after = "60"
Accept the connection, destroy the socket, send nothing Does not open: ERR_EMPTY_RESPONSE, document becomes chrome-error:// TypeError: Failed to fetch
Respond with bytes that are not HTTP Does not open TypeError: Failed to fetch

Three conclusions:

  1. A number versus no number is two different failures. Any 5xx means at least one machine spoke HTTP and said “I cannot do this”. ERR_EMPTY_RESPONSE / Failed to fetch means no HTTP reply arrived at all — that is TCP, TLS, or a process-level problem. “It says 502” and “it says this site can’t be reached” are not the same ticket, and the second one starts at the network and the process list, not the application log.
  2. fetch() does not reject on 5xx. 429, 500, 502 and 503 all resolve happily with ok: false. If nothing checks response.ok, no catch is going to surface the outage for you.
  3. The error page may be your own. Chrome renders a bodied 502 exactly as sent, so the “temporarily unavailable” page you are looking at was written by your proxy or your app — its wording tells you which product is in front. (Do not generalise this to other browsers: with an empty body some of them substitute their own page, so test yours.)

3. The browser will not back off for you

Retry-After is advice to your code, not an instruction the browser obeys. I counted arrivals on the server side for the batch above: each URL was hit exactly once per request, including the 503 carrying Retry-After: 120 and the 429 carrying 60. Nothing waited; nothing retried.

Backoff is code you write, and three parts of it are easy to get wrong:

async function withBackoff(fn, tries = 5) {
  for (let i = 0; i < tries; i++) {
    const res = await fn();
    if (res.ok) return res;
    if (res.status !== 429 && res.status !== 503) throw new Error('HTTP ' + res.status);
    const ra = Number(res.headers.get('retry-after'));
    const ms = Number.isFinite(ra) ? ra * 1000 : Math.min(30000, 500 * 2 ** i);
    await new Promise(r => setTimeout(r, ms + Math.random() * 250));  // jitter, so they don't wake together
  }
  throw new Error('gave up');
}

Back off on 429 and 503 only (a 400 or 422 will not become correct on the hundredth try); prefer the server’s Retry-After over your own exponential curve (“wait 120 seconds” carries more information than 2^n); and add jitter — ten thousand clients coming back 500 ms after the same 503 manufactures the outage a second time.

4. A 502 on a subresource just looks like “that bit didn’t appear”

I also served a normal 200 page that referenced an image returning 502, a script returning 500, and an iframe pointing at the empty 502:

  • the outer document rendered fine (document.title present, body present);
  • the <img> reported complete === true, naturalWidth === 0, and its onerror fired;
  • the iframe’s document existed with 0 characters.

A 200 main document does not mean the screen is healthy. A broken image, a chart that never draws, or a page with no styling is often one subresource eating a 502/504, and none of it becomes an error page. The first stop for those symptoms is the Network panel filtered by status, not the application log.

5. Locate the hop before you touch anything

In order, each step narrowing the range:

  1. curl -i down the normal path and copy the response headers. Server:, Via:, X-Request-Id, or a CDN’s own header (cf-ray, x-amz-cf-id) names the machine that answered.
  2. Connect straight to the origin, bypassing the front layer (--resolve, hosts file, internal address). Works direct but fails through the proxy → the middle hop’s upstream config or health check; fails direct too → the application.
  3. Time it. A 504’s latency sits right at some hop’s timeout (a proxy waiting 60 s for the backend answers in 60 s flat); a 502 is often in milliseconds because the connection was reset immediately. Those two point at different fixes: slow queries versus nobody listening on the port.
  4. Check upstream process health, then worker and connection exhaustion. A 502 that only appears at peak usually means the machine in front ran out of usable workers, not that this request was malformed.
  5. Match timestamps across logs. Use the response Date or request id to find the same call in the origin log. If the request is not there, that absence is the finding: it never reached the application. When the log stores 10- or 13-digit timestamps, convert them to your wall clock before ordering events — comparing a UTC line against your local watch silently shifts everything by hours.

6. So may I reload?

“Safe to resend” is a property of the code, not of your situation:

  • Resend as-is — 408, 425, 502, 504. The server did not actually do anything last time.
  • Wait out Retry-After — 429, 503. Waiting is the entire point of those two responses; hammering turns a 503 into a stampede.
  • Check for side effects first — 500, 409, 422, 428, 431, 451, 507, 511. The request may have failed halfway through, and resending performs the work twice.

That layer is a property of the digit, not a guarantee: a 502 that happens after the application already wrote to the database is exactly what the “safe to resend” label cannot see. On writes, look for the idempotency key before you replay.

7. A few of these are not yours to fix

  • 499 — not a standard code; nginx logs it when the client hung up before the server answered. It exists only in logs and no browser will ever show it. The question is why the client left: its own timeout, an aborted fetch, or the user navigating away.
  • 511 — a captive portal (hotel, airport) asking you for something. If unrelated sites keep returning it, you are behind the portal, not behind those origins.
  • 451 — removed over a legal claim. Not a defect; stop troubleshooting it.

Look the number up before you act

For an unfamiliar digit, the online HTTP status code lookup takes a three-digit code or a pasted status line (HTTP/1.1 302 Found) or nginx log fragment, and gives the class it belongs to, when a server emits it, what the client does next, where to look first — and that fourth, most expensive column: whether this request may be resent. It never sends a request: a URL you paste is parsed as text only, and what you type never leaves this device. Codes outside its table (anything from 100 to 599) still get their class explained, because the first digit is what determines the behaviour anyway.

Why does the error page say nginx when my origin runs something else? Because that page was written by the hop in front of you. 502 and 504 come from the proxy, which ships its own default error page — and, as section 2 measured, a bodied 5xx is displayed verbatim. The wording is a useful clue, not noise.

Does the browser obey Retry-After? No. Both measurements above confirm it: one request in, one response out, and the header handed to your code untouched.

Is reloading helping? If the code path is buggy, reload runs the same bug again. If the 503 is a deploy window, waiting a minute is correct. Only the log and the Server: header distinguish those; “reload a few more times” is not an instrument.

Can status codes be a monitoring signal? Yes, if you split them by class and by origin. Rising 4xx is usually clients or crawlers; rising 5xx is the server. And when your edge rewrites error pages and status codes, origin 5xx and user-visible 5xx are two different series — watching only one will mislead you.

Support on PayPal