HTTP Status Codes

Your monitoring dashboard alerts on a spike of 502s — what does that actually tell you? It says your reverse proxy can’t reach the backend, which is very different from a 503 (your server is overloaded) or a 504 (your backend is just slow). Status codes are how the entire web — browsers, CDNs, load balancers, retry libraries, monitoring tools — make automatic decisions about what to do next. Picking the wrong one breaks retry logic, caching behavior, and on-call dashboards.

Status codes are three-digit integers in the response status line. The first digit defines the class.

HTTP/1.1 404 Not Found
         ↑↑↑
         class = 4 (client error)
ClassRangeMeaning
1xx100–199Informational — request received, continue
2xx200–299Success — request understood and processed
3xx300–399Redirection — further action required
4xx400–499Client Error — bad request from the client
5xx500–599Server Error — server failed a valid request

1xx — Informational

CodeNameDescription
100ContinueServer received request headers; client should send the body. Used with Expect: 100-continue to avoid sending large bodies on auth failure.
101Switching ProtocolsServer is switching protocols per client request. Used to upgrade from HTTP/1.1 → WebSocket or HTTP/2.
103Early HintsServer sends preliminary response headers before the final response. Allows client to preload resources while server prepares the full response.

2xx — Success

CodeNameDescription
200OKStandard success. Body present. Returned for GET, POST, PUT, PATCH.
201CreatedResource successfully created. Location header should point to the new resource. Returned for POST/PUT.
202AcceptedRequest accepted for processing, but processing not complete. Used for async jobs. No body required.
204No ContentSuccess, no body. Common for DELETE and PUT responses where the client doesn’t need the updated resource.
206Partial ContentResponse contains a partial resource body. Used with Range requests for resumable downloads and video scrubbing.

3xx — Redirection

The client must take additional action to complete the request.

CodeNamePermanent?Method Preserved?Description
301Moved Permanently❌ (may change to GET)Resource permanently at new URI. Browsers and crawlers update bookmarks/indexes.
302Found❌ (may change to GET)Temporary redirect. Original URI should still be used for future requests.
303See Other❌ (always GET)Redirect after a POST to a GET resource. Prevents re-submission on refresh. POST → redirect → GET pattern.
304Not ModifiedResource not changed since If-Modified-Since or If-None-Match. Client uses cached version. No body returned.
307Temporary RedirectTemporary redirect; method and body preserved. Use instead of 302 if method must not change.
308Permanent RedirectPermanent redirect; method and body preserved. Use instead of 301 if method must not change.

301 vs 302 vs 307 vs 308

PermanentMethod Preserved
301
302
307
308
ℹ️

Use 308 when permanently moving a POST/PUT endpoint. Use 307 for temporary redirects that must preserve the HTTP method. When in doubt between 301 and 302, prefer 308 and 307 respectively — they are strictly correct.

ℹ️

304 returns no body. The browser uses its cached copy. Content-Type, Content-Length, and Content-Encoding headers are also omitted. Triggered by conditional request headers (If-None-Match, If-Modified-Since).

4xx — Client Error

The request is invalid or the client lacks permission.

CodeNameDescription
400Bad RequestMalformed syntax, invalid parameters, validation failure. Most common catch-all for bad input.
401UnauthorizedAuthentication required or failed. Response must include a WWW-Authenticate header describing the scheme.
403ForbiddenAuthenticated but not authorized. The server understands the request but refuses it.
404Not FoundResource does not exist at this URI. Also used intentionally to hide existence of unauthorized resources.
405Method Not AllowedHTTP method not supported on this resource. Response must include Allow header listing valid methods.
408Request TimeoutServer timed out waiting for the client to send the full request.
409ConflictState conflict, e.g., duplicate entry, concurrent write conflict, or version mismatch.
410GoneResource permanently deleted and will not return. Unlike 404, explicitly signals the resource existed.
411Length RequiredServer requires Content-Length header; client didn’t send one.
413Content Too LargeRequest body exceeds the server’s limit. Common for file upload endpoints.
415Unsupported Media TypeServer can’t process the request body’s Content-Type.
422Unprocessable EntityRequest is syntactically valid but semantically incorrect (e.g., failed business validation). Common in REST APIs.
429Too Many RequestsRate limit exceeded. Response should include Retry-After header.

401 vs 403

401403
Identity known?No (or failed auth)Yes
Can re-auth help?
Meaning“Who are you?”“I know you, but no.”

5xx — Server Error

The server failed to fulfill a valid request.

CodeNameDescription
500Internal Server ErrorGeneric server-side failure. Unhandled exception. Default error when nothing else fits.
501Not ImplementedServer doesn’t support the requested method. Different from 405 (method exists but not on this resource).
502Bad GatewayUpstream server returned an invalid response. Seen when a reverse proxy (Nginx, API Gateway) can’t reach the backend.
503Service UnavailableServer temporarily unable to handle requests — overloaded or in maintenance. Include Retry-After if possible.
504Gateway TimeoutUpstream server didn’t respond in time. Seen at load balancers and API gateways when backend is slow/unresponsive.
507Insufficient StorageServer can’t store the representation (WebDAV). Occasionally used for quota-exceeded scenarios.

500 vs 502 vs 503 vs 504

CodeWho failedWhen you see it
500Application codeUnhandled exception, crash
502Upstream serverInvalid/corrupt response from backend
503This serverOverloaded, in deploy/restart
504Upstream serverBackend too slow (timeout)

Quick Reference

CodeTypical API Use
200Successful GET, POST, PUT
201Resource created (POST)
204Delete succeeded / update with no body
206Partial file download, video range request
301Permanent domain or URL change
304Conditional GET cache hit
400Validation error, malformed JSON
401Missing or invalid token
403Valid token, wrong permissions
404Resource not found
409Duplicate record, optimistic lock conflict
422Semantic validation failure
429Rate limit hit
500Unhandled server exception
502Reverse proxy can’t reach backend
503Server overloaded or deploying
504Backend timed out
ℹ️

Interview tip: “401 = ‘who are you?’ (auth missing), 403 = ‘I know you, but no’ (authorized but denied). For validation: 400 = malformed syntax, 422 = semantically invalid. Redirects on POST/PUT: use 307/308 (preserve method) instead of 301/302. Server errors: 502 = upstream garbage, 503 = I’m overloaded (include Retry-After), 504 = upstream timeout. Distinct codes drive distinct on-call runbooks.”

Test Your Understanding

Your API returns 200 for everything — success and errors — with an error field in the JSON body. A teammate says this is fine because ‘clients parse the body anyway.’ What breaks?

Everything that automates based on status codes breaks:

  1. CDN caching — CDNs cache 200 responses. Your error response gets cached and served to other users.
  2. Retry logic — HTTP client libraries and middleware retry on 5xx but not on 2xx. Your errors never trigger retries.
  3. Monitoring/alerting — dashboards count 5xx rates. Your errors show as 0% error rate despite real failures.
  4. Load balancer health checks — passive health checks count 5xx responses to detect failed backends. Your “200 with error” bypasses detection.
  5. Browser behavior304 Not Modified saves bandwidth via conditional requests. If everything is 200, conditional caching doesn’t work.

HTTP status codes are a machine-readable API contract, not just a human convention.

A client receives a 301 redirect from POST /api/v1/orders to /api/v2/orders. The client follows the redirect, but the order isn’t created. What happened?

301 (and 302) do not preserve the HTTP method. Per spec, browsers and many HTTP clients convert the redirected request from POST to GET. The client follows the redirect with GET /api/v2/orders instead of POST /api/v2/orders — no request body, no order created.

Fix: Use 308 Permanent Redirect (or 307 for temporary). Both preserve the original method and body. The client follows the redirect with POST /api/v2/orders and the order is created.

This is a real-world bug that has broken payment flows and form submissions. The 307/308 codes were specifically created to fix this ambiguity in 301/302.

Your monitoring alerts on 502 errors. The on-call engineer checks the backend and it’s running fine — healthy, responding to local requests. Where’s the problem?

502 Bad Gateway means the reverse proxy/load balancer received an invalid response from the backend, not that the backend is down. Possible causes:

  1. Backend returned a malformed HTTP response — missing headers, truncated body, or non-HTTP response on an HTTP port
  2. Connection reset by backend — backend accepted the connection but closed it before sending a response (crash during processing, timeout, or connection limit hit)
  3. Protocol mismatch — the proxy expects HTTP/1.1 but the backend speaks HTTP/2 (or vice versa). Nginx’s proxy_pass doesn’t support HTTP/2 to backends — use grpc_pass for gRPC.
  4. Backend timeout + proxy timeout misconfiguration — the backend timed out internally and sent a partial response that the proxy couldn’t parse

The backend appears “healthy” to local requests because the issue is in the proxy ↔ backend connection path, not the backend itself. Check proxy error logs, not just backend health checks.

A rate-limited client receives 429 Too Many Requests but keeps retrying immediately, making the overload worse. What header should you include, and what retry strategy should the client implement?

Include Retry-After: N (seconds to wait) or Retry-After: <HTTP-date>. This tells the client exactly when it’s safe to retry.

The client should implement exponential backoff with jitter:

  1. First retry: wait Retry-After seconds (or 1s if absent)
  2. Subsequent retries: double the wait time (2s, 4s, 8s, …) up to a max (e.g., 60s)
  3. Add random jitter (±20%) to prevent synchronized retry storms when many clients are rate-limited at the same time

Without jitter: If 1000 clients all get 429 at the same time and all retry after exactly 1 second, you get another spike of 1000 requests at t+1s. Jitter spreads retries across [0.8s, 1.2s], smoothing the retry wave.

Your API returns 404 for unauthorized resources instead of 403. A security auditor flags this. Is the auditor right?

Both approaches are valid — it depends on the threat model.

Returning 403: Confirms the resource exists but the user lacks permission. An attacker can enumerate resource IDs by distinguishing 403 (exists, no access) from 404 (doesn’t exist).

Returning 404: Hides the existence of resources the user can’t access. The attacker can’t distinguish “doesn’t exist” from “exists but forbidden.” This is called resource enumeration protection and is the approach GitHub, AWS, and most cloud APIs use for sensitive resources.

The auditor is wrong if the API intentionally uses 404 for security (documented behavior). The auditor is right if the API inconsistently returns 403 for some unauthorized resources and 404 for others — the inconsistency leaks information about which resources exist.