The five classes
| Class | Means | Typical codes |
|---|---|---|
| 1xx | Informational; the request is still in progress | 100 Continue, 101 Switching Protocols |
| 2xx | The request succeeded | 200 OK, 201 Created, 204 No Content |
| 3xx | A different resource or URL answers | 301 Moved Permanently, 304 Not Modified |
| 4xx | The request is wrong or not allowed | 400, 401, 403, 404, 429 |
| 5xx | The server failed to fulfil a valid request | 500, 502, 503, 504 |
The class is the contract: a 4xx means the client should change the request, a 5xx means retrying the same request may work later. Returning a 5xx for a validation error tells the client to retry something that will never succeed; returning a 4xx for a temporary outage tells it to give up on a request that would have worked.
401 versus 403
401 Unauthorized means authentication is missing or invalid — the client should authenticate and try again. 403 Forbidden means the server knows who you are and refuses: authenticating again will not help. The names are misleading (401 is about authentication, not authorisation), which is why APIs that return 401 for “you are logged in but not allowed” send clients into a login loop.
A related trap is 404 versus 403 for a resource the user may not see. A 403 confirms it exists; a 404 hides it. Neither is universally right — it is an information-disclosure decision, not a protocol one.
Redirects are not interchangeable
301 is permanent and cacheable; 302 is temporary. 307 and 308 preserve the method and body, which matters for a POST: a 302 is historically followed as a GET by many clients, silently dropping the body. Use 308 for a permanent method-preserving redirect, 307 for a temporary one, and 301/302 only when a GET is intended.
A redirect chain costs latency and, for a crawler, dilutes the signal. Point at the final URL rather than chaining through three intermediate paths; the same applies to a site migration, which is why a map of old to final URLs is better than a series of hops.
Reading status codes in logs
Aggregate by class first: a rising 5xx rate is a server problem, a rising 4xx rate is usually a client or a contract change. A single 499 (client closed the request, in nginx logs) is not an application error; a burst of them is a latency symptom. Alerting on “any non-2xx” produces noise that hides the one class that needs a page, which is why the classes exist.
Caching and conditional requests
304 Not Modified is not an error and not a success with a body: it tells the client that its cached
copy is still valid. It depends on the request carrying If-None-Match or
If-Modified-Since, and on the server comparing them. A 304 without a prior cache is
meaningless, and a 200 that ignores the conditional headers wastes the bandwidth the mechanism exists
to save.
Cache headers and status codes have to agree. A 301 is cacheable by default; a 302 is not; a 500 should not be cached; a 404 may be cached and then keep a fixed link broken longer than necessary. When a route changes behaviour, revisit the cache policy as well as the handler, or clients will keep the old answer without asking again.
Retries and error bodies
429 Too Many Requests and 503 Service Unavailable should carry a Retry-After header when
the wait is known. A client that retries a 500 immediately and repeatedly turns a transient failure
into an outage; exponential backoff with jitter is the usual remedy. A 200 with an error object in
the body is the anti-pattern: every client sees success, monitoring sees success, and only the
application notices something failed. Use the status line for the outcome and the body for details.
The reference page lists the codes with their meanings and classes; use it to check the exact code rather than picking the nearest familiar number. The difference between 422 and 400, or 502 and 504, is the difference between a client fix and an infrastructure fix.