HTTP status codes explained: what to return and when
By Uxair Ijaz · 2026-11-06 · 10 min read
Status codes are the part of HTTP everyone uses and few use precisely. The difference between 400 and 422, or 401 and 403, decides whether a mobile app shows the right error, whether a monitoring alert fires, and whether a retry loop melts your production database. This guide covers every code worth using in API work, the mistakes even experienced teams make, and what clients do with each one.
The five classes
The first digit is the class:
| Class | Meaning | Who acts |
|---|---|---|
| 1xx | Informational — keep the connection open | Frameworks, not you |
| 2xx | Success | Client proceeds |
| 3xx | Redirect — go elsewhere | Client (and caches for 301/308) |
| 4xx | Client error — the request was wrong | Client must change something |
| 5xx | Server error — the server failed | Client may retry later; you get paged |
The class boundary carries the real meaning: 4xx says "the caller can fix this", 5xx says "the callee must fix this". Monitoring tools, retry libraries and browser caches all read it exactly that way — which is why returning 200 with {"error": "something failed"} is the single most damaging habit in API design: it makes success indistinguishable from failure to every machine on the path.
2xx — success, precisely
200 OK — the default success. Use for reads and for updates that return the updated resource.
201 Created — the request created a resource. Send a Location header pointing at it:
HTTP/1.1 201 Created
Location: /orders/42
{ "id": "42", "status": "placed" }
202 Accepted — the work started but has not finished: report generation, webhook dispatch, anything queued. Pair it with a status endpoint the client can poll.
204 No Content — success with nothing to say. DELETE responses are the classic case. Careful: a 204 body must be empty; clients that try to parse one get confusing errors.
206 Partial Content — byte ranges for large downloads; mostly the domain of file servers.
What professionals choose: 201 + Location on create (never a bare 200), 204 on delete, and 202 + status URL for anything asynchronous.
3xx — redirects, and the cache question
The pair that matters:
301/302: moved permanently / found — clients may change the method to GET on the retry (browsers do this for 301/302 after POST, historically).307/308: temporary / permanent — the method is preserved exactly.
If you move POST /v1/payments to /v2/payments, 308 is correct and 301 would silently turn retries into GETs. For URL shorteners and moved web pages, 301 is what caches honour permanently — test with care, because fixing a wrong 301 means waiting out every cache on the path.
304 Not Modified is how conditional GET works: the client sends If-None-Match: "abc", the server answers 304 with no body, and caches along the way save the bandwidth. Free performance for read-heavy APIs.
4xx — the caller's problem
400 Bad Request — malformed syntax: broken JSON, a query parameter that is not a number. If the request is well-formed but fails your business rules, that is 422.
401 Unauthorized — really "unauthenticated": no credentials, expired token, bad signature. The name is wrong and the industry has agreed to live with it. Always pair with WWW-Authenticate: Bearer.
403 Forbidden — authenticated but not allowed. The token is fine; this account cannot touch this resource. Do not leak existence: for data belonging to someone else, returning 404 instead of 403 is the safer convention (it hides that the resource exists).
404 Not Found — no such resource. Also the honest answer to DELETE /orders/999999 for an order that never existed.
409 Conflict — the request collides with current state: creating an account whose email is taken, cancelling an already-shipped order. The body should explain the conflict.
410 Gone — deliberately removed and not coming back. Useful for retired API versions: /v1/orders answering 410 documents itself.
415 Unsupported Media Type — you sent XML to a JSON endpoint. Pair with an error naming the accepted types.
422 Unprocessable Content — syntactically valid, semantically wrong: negative quantity, unknown SKU, dates in the wrong order. This is where field-level validation errors belong:
HTTP/1.1 422 Unprocessable Content
{ "error": "validation_failed",
"fields": { "qty": "Must be at least 1", "sku": "Unknown SKU" } }
429 Too Many Requests — rate limiting. Send Retry-After: 30 and, ideally, RateLimit-Remaining/RateLimit-Reset. Clients that respect it are the well-behaved ones; the header is what makes them so.
The 400 vs 422 debate: both appear in production APIs and both are defensible — Rails made 422 the validation default, while many JSON APIs use 400 for everything syntactically fine but semantically wrong. What professionals actually choose: one of the two, consistently, documented — and never 200 with an error body.
5xx — the server's problem
500 Internal Server Error — the unhandled exception. Never include stack traces in the body; log them, return a reference ID.
502 Bad Gateway / 504 Gateway Timeout — the proxy upstream failed or timed out. These are usually your infrastructure talking; a client seeing them should retry with backoff.
503 Service Unavailable — deliberate: maintenance mode or overload. Retry-After: 300 makes it graceful.
Which codes do clients automatically retry? The safe answer: 5xx and 429 (honouring Retry-After), and never 4xx. Idempotent requests (GET, PUT, DELETE) are safe to retry on any timeout; POST needs an idempotency key before a retry loop is safe.
The mistakes that cost real time
| Mistake | What it breaks | Do instead |
|---|---|---|
200 with {"success": false} |
Monitoring, caches, retry logic | Real status code; details in body |
401 for permission problems |
Clients loop refreshing tokens | 403 (or 404 to hide existence) |
404 for a wrong method |
Client thinks the resource is gone | 405 Method Not Allowed + Allow header |
400 for validation misses |
No field-level detail contract | 422 with a fields object |
302 on a moved API endpoint |
POSTs become GETs mid-retry | 307/308 |
500 with a stack trace |
Information disclosure | Reference ID in body, trace in logs |
The working set
If you internalise only this much, you will be right in 95% of situations: 200, 201 + Location, 204, 304; 301/308 for moves; 400, 401, 403, 404, 409, 422, 429 + Retry-After; 500, 502, 503 + Retry-After. Everything else is a corner worth knowing exists — the MDN status reference is the honest complete list.
The body is half the contract too: what those codes should contain — bare resources vs envelopes, error shapes, pagination — is the subject of our guide to designing API responses. And when an API hands you JSON that does not parse, our JSON validator pinpoints the offending line and column before you blame the status code.