The HTTP status codes that actually matter

The status codes that actually come up when you are debugging an API, what each one is really telling you, and which are routinely misused.

Updated

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

CodeMeaningRetry?
200OK
201Created — expect a Location
202Accepted, processing laterPoll instead
204Success, no body
301 / 308Moved permanentlyFollow, update your URL
302 / 307Moved temporarilyFollow, keep your URL
304Not modified — your cache is valid
400Malformed requestNo — fix the request
401Not authenticatedYes, with credentials
403Not permittedNo
404Not foundNo
405Wrong method for this URLNo
409Conflict with current stateYes, after re-reading
410Permanently goneNo
415Wrong Content-TypeNo — fix the header
422Valid syntax, invalid valuesNo
429Rate limitedYes — honour Retry-After
500Server errorMaybe, with backoff
502 / 503 / 504Upstream down, overloaded, slowYes, 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.

Frequently asked questions

Should a REST API return 200 or 201 after creating a resource?

201 Created, with a Location header pointing at the new resource. Returning 200 is not fatal, but 201 tells the client a resource now exists at a URL it can follow, which 200 does not.

What is the difference between 401 and 403?

401 Unauthorized means the server does not know who you are — credentials are missing, malformed or expired. 403 Forbidden means it knows exactly who you are and you are not allowed. If retrying with a fresh token could help, it is a 401.