HTTP status codes: what to send and what to do

A status code is the first thing a client reads and the last thing many APIs get right. The class tells you who is responsible; the specific code tells you what to do next.

The five classes

ClassMeansTypical codes
1xxInformational; the request is still in progress100 Continue, 101 Switching Protocols
2xxThe request succeeded200 OK, 201 Created, 204 No Content
3xxA different resource or URL answers301 Moved Permanently, 304 Not Modified
4xxThe request is wrong or not allowed400, 401, 403, 404, 429
5xxThe server failed to fulfil a valid request500, 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.

Questions this guide raises

What is the difference between 401 and 403?

401 means the request lacks valid authentication: send credentials and retry. 403 means the server authenticated the request and refuses it: retrying with the same credentials will not help. Returning 401 for a permission failure causes clients to loop through login.

Which status codes are safe to retry?

Generally 5xx (except a definitive 501), 429 with Retry-After, and network timeouts — and only when the request is idempotent or the API supports idempotency keys. A 4xx other than 408/429 will not succeed on retry without changing the request.

Why is returning 200 with an error body a problem?

Because every layer that reads the status treats the call as successful: the client skips its error path, monitoring records a success, and retries are never triggered. The error is visible only to code that parses the body. Put the outcome in the status line and the detail in the body.