Most developers can recite 200, 404, and 500 without thinking. Fewer can confidently explain the difference between 401 and 403, or when 422 is more correct than 400. Status codes are one of the few places in API design where there's an actual standard to follow (RFC 9110) rather than a convention teams invent themselves — yet they're routinely used inconsistently, which makes client-side error handling harder than it needs to be.
The Five Classes
Every status code falls into one of five ranges, and the first digit alone tells a client how to treat the response before it even reads the body:
- 1xx — Informational. Rare in application code; mostly protocol-level (
100 Continue). - 2xx — Success. The request was received, understood, and accepted.
- 3xx — Redirection. Further action is needed to complete the request.
- 4xx — Client Error. The request itself was wrong in some way.
- 5xx — Server Error. The request may have been fine, but the server failed to process it.
This classification matters for retry logic: a well-behaved client should generally retry on 5xx (the server might recover) but not blindly retry on 4xx (the request needs to change first, or it'll just fail the same way again).
The Codes People Actually Confuse
200 vs 201 vs 204. 200 OK is a generic success. 201 Created should be returned specifically after a POST that creates a new resource, ideally with a Location header pointing to it. 204 No Content means success with deliberately no response body — correct for a DELETE that succeeded, wrong for an endpoint that has data to return but the developer forgot to send it.
401 vs 403. This is the most commonly swapped pair. 401 Unauthorized actually means unauthenticated — the server doesn't know who you are (missing or invalid credentials). 403 Forbidden means the server knows exactly who you are, and you're not allowed to do this. Returning 403 when a token is simply missing is misleading to API consumers, who will look for a permissions problem that doesn't exist.
400 vs 422. 400 Bad Request is for malformed requests the server can't even parse — invalid JSON, missing required structure. 422 Unprocessable Entity is for requests that are syntactically valid but semantically wrong — well-formed JSON with a field value that fails business validation (an email field containing a number, for instance). Not every API distinguishes these, but if yours does, it gives clients a cleaner way to tell "you sent garbage" apart from "you sent a well-formed request I still can't accept."
404 vs 410. 404 Not Found is the default for "this doesn't exist," but says nothing about whether it ever did. 410 Gone explicitly signals that a resource used to exist and was intentionally removed — useful for APIs versioning out old endpoints, since it tells the caller not to bother retrying or looking elsewhere.
429 vs 503. 429 Too Many Requests means the client is being rate-limited — expected to back off and retry, usually with a Retry-After header. 503 Service Unavailable means the server itself is overloaded or down for maintenance, independent of anything the client did. Both suggest retrying later, but conflating them makes it harder for a client to distinguish "I'm sending too many requests" from "their infrastructure is down."
A Few Codes Worth Knowing Beyond the Basics
409 Conflict — the request conflicts with current server state (e.g. duplicate resource)
415 Unsupported Media Type — the Content-Type header isn't something the server accepts
429 Too Many Requests — rate limit exceeded
451 Unavailable For Legal Reasons — content blocked for legal/regulatory reasons
502 Bad Gateway — an upstream server returned an invalid response
504 Gateway Timeout — an upstream server didn't respond in time
502 and 504 are especially useful to distinguish in a microservices setup: 502 tells you an upstream service responded with something malformed or errored; 504 tells you it simply never responded in time. Logging these separately makes debugging cascading failures noticeably faster.
Designing Your Own API's Error Responses
Status codes alone are rarely enough context for a client to build a good error message — pair the code with a structured, consistent error body:
{
"error": {
"code": "invalid_email",
"message": "The email field must be a valid email address.",
"field": "email"
}
}
Keep the HTTP status code as the category of failure and the body as the specific reason. This lets clients branch on status code for broad handling (show a generic "try again" on any 5xx) while still surfacing the precise message when one is available.
Summary
- The first digit of a status code tells a client how to react before reading the body — 4xx generally shouldn't be retried unchanged, 5xx often can be.
- 401 means "who are you?", 403 means "I know who you are, and no."
- 400 is for malformed requests; 422 is for well-formed requests that fail validation.
- 404 says nothing about history; 410 explicitly means "this used to exist."
- Pair a well-chosen status code with a structured, consistent error body — the code and the message are doing different jobs, not the same one.