HTTP Status Codes: When You Actually See Each One
A practitioner's guide to the HTTP status codes that actually appear in production: the everyday 2xx, the method-preserving distinction between 301/302 and 307/308, conditional 304s, the 400-vs-422 and 401-vs-403 and 404-vs-410 confusions, 429 with Retry-After, and which of 500/502/503/504 comes from the load balancer versus the origin.
RFC 9110 defines roughly sixty HTTP status codes, but a working REST API only leans on a dozen. This is the practitioner's map. Success: 200 OK is the everyday success. 201 Created answers a POST that materialized a new resource, paired with a Location header. 204 No Content fits a successful DELETE or PUT with nothing to return. Redirects: 301 is permanent, 302 temporary — but both are historically buggy because most clients silently rewrite POST to GET when following them. When method and body must survive — webhook receivers, POST APIs — use 307 Temporary Redirect or 308 Permanent Redirect, which RFC 9110 requires clients to follow with the original method intact. 304 Not Modified is the conditional-GET response: the server matched the client's ETag via If-None-Match and is saying the cached copy is still good. See HTTP Caching for the conditional-request machinery. Client errors and the confusions that ship bugs: 400 Bad Request vs 422 Unprocessable Entity. 400 means the request is structurally broken — malformed JSON, bad encoding. 422 means the syntax parsed but the data failed validation (age: -5, invalid email). Many APIs collapse both into 400 and lose the distinction. 401 Unauthorized vs 403 Forbidden. 401 says "I do not know who you are" — credentials are missing or invalid, and RFC 9110 requires a WWW-Authenticate header on the response. 403 says "I know who you are and you cannot do this." Using 404 to hide a 403 is a deliberate leak-resistance pattern, not an accident. 404 Not Found vs 410 Gone. 404 is ambiguous — maybe it never existed, maybe it will return. 410 is a deliberate "permanently deleted" signal, which search engines drop from the index faster. 429 Too Many Requests is the rate-limit response. Pair it with a Retry-After Header (seconds or HTTP-date) so well-behaved clients back off correctly. Server errors and who emits them: 500 Internal Server Error comes from the application — an unhandled exception. 502 Bad Gateway is emitted by a reverse proxy or load balancer that reached the origin but got garbage or a dropped connection. 503 Service Unavailable means the upstream refused — overload, maintenance, or no healthy backends; it should carry a Retry-After. 504 Gateway Timeout is the proxy giving up after the origin failed to respond in time. 502 and 504 almost always originate at the edge, not the app. Common mistakes: returning 200 OK with an error inside the JSON body (breaks monitoring, retries, and caches); using 404 for permission denied without consciously choosing to obscure existence; emitting 503 with no Retry-After; and treating 301 and 308 as interchangeable on POST endpoints.