20 min read

Chapter 4 — HTTP Status Codes, and Who Sent Them

A Status Code Is a Claim — First Find Out Who Is Making It

Part 1: Foundations. Prerequisites: Chapters 1–3.


Opening

Every HTTP response starts with a three-digit status code. Most engineers know the famous ones — 200, 404, 500 — and treat the rest as noise. That loses most of the evidence.

A status code is a precise claim about what happened to a request. And here is the part almost everyone misses: it is a claim made by whoever produced the response — which is often not your application. Between the browser and your code sit a CDN, a load balancer, a reverse proxy, maybe an API gateway. Any of them can answer the request themselves. A 502 from your load balancer, a 403 from a CDN's firewall and a 403 from your own permission check look identical in a quick glance and mean completely different things.

This chapter gives you two skills. First, what each status code actually claims. Second — and more important — how to work out which hop sent it, because that decides where you look next.


Part 1: The Chain a Request Travels Through

A request from a browser to your API usually passes through several systems. Each one can either forward the request onward or answer it directly.

The words for these roles:

  • Client — whoever sends the request (the browser, a mobile app, another service).
  • Origin — your actual server, the thing the CDN or proxy forwards to.
  • Proxy / reverse proxy — a server that receives requests on behalf of others and forwards them. nginx and load balancers are reverse proxies.
  • Gateway — a proxy acting as the entry point to backend services. In status-code language, any proxy forwarding to an upstream is "the gateway" — which is why 502 and 504 are called Bad Gateway and Gateway Timeout.
  • Upstream — the next system toward the origin, from a given hop's point of view. To the load balancer, your app is upstream. To your app, the database is upstream.
  • Downstream — the direction back toward the client.

So 502 Bad Gateway means: "I am a proxy, and the upstream I forwarded to gave me a bad response or none." It is generated by the proxy. Your app may never have seen the request, or may have crashed handling it.


Part 2: Who Sent This Response?

Before interpreting any error status, answer this question. Four kinds of evidence settle it.

1. Response headers. Each layer tends to stamp its own:

HeaderWho it points to
server: nginx, server: cloudflare, server: VercelWhich software produced the response
cf-ray, cf-cache-statusCloudflare handled it
x-vercel-id, x-vercel-cache, x-vercel-errorVercel handled it; x-vercel-error names a platform error
x-amz-cf-id, x-cache: Error from cloudfrontAWS CloudFront
viaLists the proxies the response passed through
x-request-id / your own correlation headerYour app produced or touched it — and gives you the ID to search your logs for

2. The body. Your API returns errors in its own JSON shape, like { "error": { "code": "ORDER_NOT_FOUND" } }. A plain HTML page that says "502 Bad Gateway — nginx" or a branded CDN error page was not written by your code.

3. Timing. How long did the request take? A 504 that arrives after exactly 30.0 or 60.0 seconds is almost certainly a proxy's configured timeout, not something your code decided. An error that arrives in 3 milliseconds probably never reached a slow backend.

4. Your app's logs. Search for the request (by time, path, or request ID). If your app has no record of it, the response came from a layer in front of your app — and debugging your app's code is pointless until you know why the request never reached it.


Part 3: The Five Classes

The first digit is the class. It tells you whose fault it is likely to be and whether retrying makes sense.

ClassMeaningWhose problemRetry unchanged?
1xxInformational — "keep going"Nobody'sNot applicable
2xxSuccessNobody'sNot needed
3xxRedirection — "look elsewhere"Nobody's, usuallyThe client follows it
4xxClient error — the request is the problemThe caller's request (from the server's point of view)No — except 408 and 429
5xxServer error — the server failedA server somewhere in the chainMaybe — only if the request is safe to repeat

The 4xx/5xx split is the most useful part. A 4xx says "this exact request will keep failing until something about it changes." A 5xx says "this request may be fine; something on the server side broke."


Part 4: 1xx, 2xx and 3xx — the Ones That Still Cause Bugs

1xx — rarely seen, occasionally important

  • 101 Switching Protocols — the server agreed to upgrade the connection, most often to a WebSocket. If a WebSocket fails to connect, check whether you got 101; a proxy that does not support upgrades returns something else.
  • 103 Early Hints — the server sends hints (like "start loading this CSS") before the real response is ready.

2xx — success, with traps

CodeMeaningThe trap
200 OKSuccess, with a bodyA 200 can still carry an error in the body if an API is badly designed — always check the body, not only the status
201 CreatedA resource was created; its URL is usually in the Location header
202 AcceptedThe request was queued, not completedThe work has not happened yet. "It returned 202 but nothing happened" is often a background job that failed later
204 No ContentSuccess, with no bodyCalling response.json() on a 204 throws Unexpected end of JSON input
206 Partial ContentOnly part of the resource, as requested with a Range headerNormal for video and large downloads

3xx — redirects and caching

CodeMeaningMethod kept on redirect?Cached by browser?
301 Moved PermanentlyUse the new URL foreverBrowsers may change POST to GETYes, aggressively
308 Permanent RedirectSame as 301, but strictly keeps the method and bodyYesYes
302 FoundTemporarily elsewhereBrowsers may change POST to GETNo (by default)
307 Temporary RedirectSame as 302, but strictly keeps the method and bodyYesNo
303 See Other"Go GET this other URL" — the standard response after a form POSTAlways becomes GETNo
304 Not Modified"Your cached copy is still valid" — no body sent——

Three redirect bugs worth knowing:

  1. A wrong 301 sticks. Browsers cache permanent redirects, so even after you fix the server, users who saw the bad 301 keep being redirected until their cache clears. Use 302/307 while you are unsure.
  2. A POST redirected with 301/302 can arrive as a GET with no body. If a form submission "loses its data", check for a redirect — commonly http→https or a missing/extra trailing slash — and use 307/308 or fix the URL.
  3. fetch follows redirects silently. You get the final response, not the redirect. Check response.redirected and response.url. A response that "should be JSON" but is the login page HTML is usually a silent redirect to /login (Chapter 2).

304 and the browser cache. A conditional request asks "has this changed since my copy?" using If-None-Match (with an ETag) or If-Modified-Since. 304 means "no, use your copy." In DevTools, also watch for 200 (memory cache) or 200 (disk cache) — those were served from the browser's cache without any request at all. If a change "isn't showing up", check whether the response came from cache.


Part 5: 4xx — The Request Is the Problem

CodeNameWhat it actually claimsUsual causes
400Bad RequestThe request is malformed — the server cannot even process itInvalid JSON syntax, a missing required parameter, a wrong type
401Unauthorized"I don't know who you are." Authentication missing or invalid, despite the nameNo token, expired token, wrong token, cookie not sent (a cross-site cookie blocked by SameSite)
403Forbidden"I know who you are, and you can't do this." — or a firewall refused the requestMissing role or permission, wrong tenant, or a WAF/CDN blocking the request — check who sent it
404Not FoundNothing at this URL — or the server is hiding that it existsWrong path, wrong base URL, trailing slash, the resource was deleted, a route that only exists in another environment, or a permission check that returns 404 on purpose
405Method Not AllowedThe URL exists but not with this method; the Allow header lists valid onesPOST to a GET-only route; a CORS preflight OPTIONS the server does not handle
408Request TimeoutThe client was too slow to send the requestSlow uploads, stalled connections. Safe to retry
409ConflictThe request conflicts with the current stateA duplicate, a version mismatch, an action on something already changed
410GoneIt existed and was removed permanentlyDeleted resources, retired API versions
412Precondition FailedA condition like If-Match was falseOptimistic concurrency: the resource changed since you read it — see SD QA Q27
413Content Too LargeThe body exceeds a size limitUsually a proxy limit, not your code — nginx defaults to 1 MB (client_max_body_size); serverless platforms cap request bodies too
414URI Too LongThe URL is too longHuge query strings — move data to the body
415Unsupported Media TypeThe server does not accept this Content-TypeSending form data to a JSON endpoint, or forgetting Content-Type: application/json
422Unprocessable ContentThe request is well-formed but invalidValidation failures: email format, missing business fields. (Some APIs use 400 for this instead — conventions differ)
429Too Many RequestsYou hit a rate limit; Retry-After may say how long to waitRetrying too aggressively, a loop, a bot, or a shared IP. Retry after waiting
431Request Header Fields Too LargeHeaders are too bigAlmost always too many or too large cookies accumulated on the domain
499Client Closed RequestNot standard — nginx's log code for "the client disconnected before I answered"The user navigated away, or the client's timeout is shorter than the server's processing time

401 versus 403 — the pair everyone mixes up

401 Unauthorized403 Forbidden
Really meansNot authenticatedNot authorised
The server's view"Who are you?""I know who you are. No."
FixLog in, refresh the token, send the credentialsGet the permission, use the right account or tenant
Debug firstIs the token or cookie actually sent? Is it expired?Which permission check failed — and did your app or a firewall send it?

The 404 that only happens on refresh

A single-page app works when you click around, but refreshing /dashboard/settings gives 404. The browser asked the server for /dashboard/settings as a real file, and a static host has no such file — the route only exists inside the JavaScript router. The host must be configured to serve index.html for unknown paths.


Part 6: 5xx — Something on the Server Side Failed

CodeNameWho usually sends itWhat it claims
500Internal Server ErrorYour app (or the platform running it)An unhandled error while processing the request. The answer is in your app's logs
501Not ImplementedServerThe server does not support this method or feature at all
502Bad GatewayA proxyThe upstream gave an invalid response or none: it crashed, refused the connection, reset it, or closed a reused connection
503Service UnavailableA proxy or your appNot able to handle it right now: no healthy upstream, overloaded, maintenance, or deliberate load shedding. May include Retry-After
504Gateway TimeoutA proxyThe proxy gave up waiting for the upstream within its timeout

502 versus 504 — fast failure or slow failure

  • 502 usually arrives quickly. The proxy tried the upstream and got a refused or broken connection. Look at whether the app is running and healthy: crashes, restarts, deploys, ECONNRESET, keep-alive mismatches (Chapter 3).
  • 504 arrives after the proxy's timeout. The upstream accepted the request and did not answer in time. Look at why the app is slow: a slow query, a lock, a hung call to a third-party API, an exhausted connection pool.

The dangerous thing about 504

A 504 means the proxy stopped waiting. It does not mean your app stopped working. The app may still be processing — and may finish successfully after the client has already been told it failed.

#
Browser
Load balancer (30s timeout)
API server
Payment provider
1
POST /checkout
forwards to API
1 / 9

Idempotency keys are covered in depth in SD QA Q26.

Platform and CDN codes

CDNs and platforms add their own codes and labels. Learn the ones for whatever you deploy on.

  • Vercel names platform errors in the x-vercel-error header and the error page — for example FUNCTION_INVOCATION_TIMEOUT (504: your function exceeded its time limit), FUNCTION_INVOCATION_FAILED (500: your function crashed), FUNCTION_PAYLOAD_TOO_LARGE (413).
  • Cloudflare uses the non-standard 52x range between Cloudflare and your origin: 520 (the origin returned something unexpected), 521 (the origin refused the connection — it is down), 522 (connecting to the origin timed out), 524 (connected, but the origin took too long to respond), 525/526 (TLS problems with the origin).

A 52x tells you immediately: Cloudflare is fine; the hop from Cloudflare to your origin failed.


Part 7: Things That Look Like Status Codes but Are Not

Some of the most confusing "errors" in the Network tab are not HTTP statuses at all.

What DevTools showsWhat it means
(failed) with net::ERR_…No HTTP response arrived. Read the net::ERR_ code (Chapter 3)
CORS errorThe browser received a response but refused to let your JavaScript read it, because it lacked the right Access-Control-Allow-* headers. The server may have returned 200 — or the real error may be hidden behind CORS
A failed OPTIONS request before your real requestThe CORS preflight failed. Preflights carry no cookies or auth headers, so a server that requires auth on OPTIONS returns 401/403 and the real request never happens
(blocked:mixed-content)An https page tried to load an http resource — the browser blocks it
(canceled)The browser cancelled the request: navigation, a newer request, an AbortController
(pending) foreverNo response yet. The server is hung, or the browser is queueing the request because it already has 6 open connections to that host (the HTTP/1.1 per-host limit)
Status 0 in a library, or Axios Network ErrorNo response was received or readable — network failure or CORS. There is no status to read

CORS hides the real error. If an API returns 500 without CORS headers, the browser reports a CORS error, not the 500. Before "fixing CORS", check the request in the Network tab and in the server logs — the CORS error may be a symptom of a crash on the server.


Part 8: The Status-Code Reflex

You seeFirst questionLook at
401Was the credential actually sent?The request's Authorization header or Cookie; token expiry; cookie SameSite/Domain
403Who sent it — my app or a firewall?Response headers and body; then your permission logic
404Is the URL exactly what I think it is?The full request URL, base URL, trailing slash, environment, method
413Which layer has the size limit?Proxy and platform limits before your app
422 / 400What does the error body say?The validation message in the response
429Who is rate-limiting, and are we retrying too fast?Retry-After, the rate-limit headers, retry loops
500What did my app log for this request?App logs, by request ID
502Is the upstream running and accepting connections?Crashes, restarts, deploys, keep-alive and idle timeouts
503Is anything healthy behind the proxy?Health checks, instance count, overload
504Why is the upstream slow — and is it still running?Slow queries, locks, third-party calls, pool exhaustion; idempotency of retries
52x (Cloudflare)Why can't the CDN reach my origin?The origin's health and its connectivity from the CDN
CORS errorIs there a real server error behind it?Server logs, the preflight request, Access-Control-Allow-* headers

The Causal Chains

Self-test: predict each consequence before revealing it.
  • If you get a 502
  • If you get a 504
  • If a 504 is followed by a user retry on a write endpoint
  • If your app's logs have no record of a failing request
  • If you get 401
  • If you get 403 with an HTML page from your CDN
  • If a form POST arrives at the server as a GET with no body
  • If response.json() throws Unexpected end of JSON input on success
  • If a change on the server is not visible in the browser
  • If the browser reports a CORS error
  • If you get 431
  • If you get 413

The Rule You Keep Forever

A status code is a claim, and the first question is always who made it. Check the headers, the body, the timing and your own logs before deciding the code is about your app — and treat every 504 as "I stopped waiting", never as "nothing happened."


Vocabulary Map

TermWhat it means
Status codeA three-digit result at the start of every HTTP response; the first digit is the class
Client / originWho sends the request / the server that actually owns the content
Proxy / reverse proxyA server that receives and forwards requests on behalf of others
GatewayA proxy acting as the entry point to upstream services
Upstream / downstreamToward the origin / back toward the client, from a given hop's view
CDN / edgeA network of servers near users that caches and forwards requests
WAFWeb application firewall — rules that block suspicious requests, often at the CDN
Authentication / authorisationProving who you are (401) / being allowed to do something (403)
RedirectA 3xx response pointing the client to another URL
Conditional requestA request asking "has this changed?" using If-None-Match or If-Modified-Since
ETagA version identifier for a resource, used for caching and optimistic concurrency
Rate limitA cap on requests per time window; exceeding it returns 429
Retry-AfterA header saying how long to wait before retrying
IdempotentSafe to repeat — doing it twice has the same effect as once
Idempotency keyA client-chosen ID that lets the server recognise a retry of the same operation
CORSThe browser's rules for letting a page read responses from another origin
PreflightThe OPTIONS request a browser sends before some cross-origin requests to ask permission
Mixed contentAn https page loading http resources, which browsers block
Load sheddingDeliberately rejecting some requests (503) to protect the system under overload

Practice Problems

Problem 1 — Who sent it? A user reports a 403. The response body is an HTML page with your CDN's branding, and your app's logs have no record of the request. What does this tell you, and what are the next two things you check?

Problem 2 — Fast or slow. Two incidents: in the first, users get 502 within 50 ms; in the second, users get 504 after exactly 60 seconds. For each, give your two leading hypotheses and the first place you look.

Problem 3 — The disappearing body. A signup form works locally. In production, the server receives the request as GET /signup/ with no body. The form posts to /signup. What is happening, and how do you confirm it?

Problem 4 — CORS or crash? The browser shows a CORS error on POST /api/orders, but only for orders with more than 50 items. Smaller orders work. Why is "add the CORS header" probably the wrong first move, and what do you check instead?

Problem 5 — The 504 retry. Your mobile app automatically retries any request that fails with 504. Which endpoints does this make dangerous, and what two changes make the retry safe?


What's Coming Next

Part 1 gave you the language: the method, the errors, the codes, the statuses. Part 2 gives you the instruments. Chapter 5 opens the most-used one — the browser's DevTools — and goes through it the way a senior engineer uses it during an investigation: the Network panel's waterfall and timing breakdown, the request initiator, breakpoints that pause only when something interesting happens, and the Application panel where cookies, storage and service workers quietly cause bugs.