Status codes are the fastest diagnostic an API gives you, and most of the confusion around them comes from a handful that are routinely used to mean something they do not.
The ones you will actually see
200 OK — the request succeeded. Note what this does not promise: nothing about the body being correct or complete.
201 Created — a resource now exists. Should come with a Location header.
204 No Content — success, and there is deliberately no body. Common after a
DELETE. If your client tries to parse JSON from a 204 it will throw, which is a
surprisingly common bug.
301 vs 302 — permanent versus temporary. This one has consequences beyond your app: a 301 is cached aggressively by browsers and treated by search engines as a permanent move. Sending a 301 you later want to undo is genuinely painful.
400 Bad Request — malformed input. Often really means "I could not parse your JSON."
401 Unauthorized — badly named; it means unauthenticated. Who are you?
403 Forbidden — authenticated, but not permitted. Retrying with a new token will not help.
404 Not Found — no such resource. Also used, legitimately, to hide the existence of resources from users who are not allowed to know about them.
409 Conflict — the request collided with current state. Duplicate unique key, or an edit against a stale version.
422 Unprocessable Entity — the JSON parsed fine but the values are invalid. The useful distinction from 400 is syntax versus semantics.
429 Too Many Requests — rate limited. Should include Retry-After; honour it.
500 Internal Server Error — the server broke. Your request may be perfectly valid.
502 / 503 / 504 — an upstream is down, overloaded, or slow. These are the codes worth retrying with backoff; a 400 never is.
The rule that saves the most time
The leading digit tells you where to look before you read anything else. 4xx means change your request. 5xx means change the server. Developers lose hours debugging their own payload against what turns out to be a 502.
The pairs people mix up
Four distinctions cause most of the arguments in code review.
401 vs 403. 401 means we do not know who you are — retry with credentials. 403 means we know exactly who you are and the answer is still no — retrying is pointless. Returning 403 for an expired token sends clients down the wrong path, because the correct response to an expired token is to refresh and retry, which 403 tells them not to do.
400 vs 422. 400 is syntax: the body did not parse. 422 is semantics: it parsed fine, but
age: -5 is not a valid age. Clients handle these differently — a 400 is a bug in your
serialisation code, a 422 is something to show the user.
404 vs 410. 404 says not found, which might be temporary. 410 says this existed and is permanently gone. Search engines treat 410 as a much stronger signal to drop a URL, which matters if the endpoint ever served public content.
409 vs 422. 409 is a collision with current state — a duplicate unique key, or an edit against a version someone else has already superseded. 422 is about the values themselves, independent of what is in the database.
A quick reference
| Code | Meaning | Retry? |
|---|---|---|
| 200 | OK | — |
| 201 | Created — expect a Location | — |
| 202 | Accepted, processing later | Poll instead |
| 204 | Success, no body | — |
| 301 / 308 | Moved permanently | Follow, update your URL |
| 302 / 307 | Moved temporarily | Follow, keep your URL |
| 304 | Not modified — your cache is valid | — |
| 400 | Malformed request | No — fix the request |
| 401 | Not authenticated | Yes, with credentials |
| 403 | Not permitted | No |
| 404 | Not found | No |
| 405 | Wrong method for this URL | No |
| 409 | Conflict with current state | Yes, after re-reading |
| 410 | Permanently gone | No |
| 415 | Wrong Content-Type | No — fix the header |
| 422 | Valid syntax, invalid values | No |
| 429 | Rate limited | Yes — honour Retry-After |
| 500 | Server error | Maybe, with backoff |
| 502 / 503 / 504 | Upstream down, overloaded, slow | Yes, with backoff |
The retry column is the one worth internalising. Retrying a 4xx unchanged will fail exactly the same way every time; retrying a 5xx or 429 with exponential backoff is usually correct.
The two that catch people out
204 and JSON parsing. A 204 No Content has no body by definition. Client code that
unconditionally calls .json() on every response throws on it, and the resulting error looks
like a server problem when it is a client bug. Check for 204 before parsing.
304 and conditional requests. If you send If-None-Match or If-Modified-Since, a 304
means your cached copy is still good — it is a success, not a failure, and it deliberately has
no body. Treating it as an error is a common source of phantom bugs in code that caches.
Where status codes mislead you
Plenty of APIs return 200 OK with {"error": "not found"} in the body. Some return 200
with an empty body where the resource should be. Both pass any test that only asserts on the
status line.
This is not rare, and it is not always a mistake — GraphQL does it by design, returning 200
with an errors array. It does mean the status code alone cannot be your correctness check.
That is the case for checking the response body too, rather than trusting the code — see validating API responses against a schema, why a 200 is not enough, or start with how to test a REST API.