The ranking

If I could only teach an API designer fifteen HTTP codes, these are the fifteen. They cover maybe 99% of what any CRUD-style REST or RPC-over-HTTP API actually needs to express. Use anything else and you're either doing something unusual or misreading the spec.

The other 48 codes in the registry aren't wrong to return — many are useful in specific contexts (WebDAV, video range requests, captive portals, proxy infrastructure). They just don't belong in the memorized-core set.

2xx — Success (four of them matter)

200 OK — the default success. Any request that succeeded and has a meaningful body to return. If the word "success" doesn't have an obvious more-specific match below, use 200.

201 Created — a POST that created a new resource. Include a Location header pointing to the new resource's URL. That's a spec requirement most APIs ignore; don't be one of them. Include the created resource in the body too, because round-tripping back to GET the resource you just created is wasteful.

202 Accepted — you accepted a request but haven't done the work yet. Async jobs. Returns a Location header pointing at a status URL the client can poll. Critically, 202 says nothing about whether the work will eventually succeed — it's an acknowledgement, not a promise.

204 No Content — success with no body. DELETE that worked. A PUT where the client already has the updated state. Pure-side-effect endpoints. The body must be literally empty — not {}, not whitespace, zero bytes. Strict clients will error if anything follows.

Every other 2xx code either pertains to a rare special case (206 for range requests, 207 for WebDAV multi-status) or is effectively unused (205 Reset Content). You can usually skip them.

3xx — Redirection (four of them matter)

301 Moved Permanently — URL has a new permanent home. Search engines will update their indexes. Browsers and caches will remember this forever, so only use 301 when you're sure. For anything you might change your mind about, start with 302.

302 Found — temporary redirect. "Go here for now." Historically browsers changed POST to GET after 302 against the spec; that's why 307 exists. For human browser navigation where only GET is in play, 302 is still fine.

304 Not Modified — the conditional-GET response. Client sent If-None-Match or If-Modified-Since; the content matches; respond 304 with an empty body. Massive bandwidth saver for static assets. The common bug: your ETag includes a timestamp or random bit, so the ETag never matches and 304 never fires.

308 Permanent Redirect — like 301, but guaranteed to preserve the HTTP method. If you're redirecting a POST endpoint to a new URL and you want the POST to continue being a POST at the new URL, use 308, not 301. Same caveats about permanence — hard to undo once published.

307 (temporary with method preservation) is the 308 equivalent for 302 and you occasionally need it, but in practice 302 is fine for browser-driven flows and 308 covers the API migration case. 303 (See Other) is for the POST-Redirect-GET pattern, a narrower use case.

4xx — Client error (five of them matter)

This is where most of the action happens, and where most mistakes cluster. The core five:

400 Bad Request — the request is malformed. Body doesn't parse as JSON. Required query parameter missing entirely. Type mismatch that prevents parsing. Use 400 when you can't even start processing the request.

401 Unauthorized — you're not authenticated. No token, expired token, bad JWT signature. Badly named — "Unauthorized" sounds like 403, but it actually means "unauthenticated." Must include a WWW-Authenticate header per the spec (most real-world APIs skip this — do better). The client response: log in and retry.

403 Forbidden — you are authenticated, but not allowed to do this. User is logged in but doesn't own the resource, or doesn't have the required role. The client response: nothing to do except ask for different permissions. You can also return 404 in place of 403 when you don't want to leak the existence of a resource — useful for hiding private data.

404 Not Found — the resource doesn't exist. Typo in the URL, deleted record, wrong ID. Distinct from 410 Gone, which means "this used to exist and was intentionally removed" — 410 is a stronger signal to search engines. If you don't know whether the resource ever existed, 404.

429 Too Many Requests — the client is over a rate limit. Always include a Retry-After header (seconds or HTTP date). Including X-RateLimit-Remaining-style headers is nice but non-standard — pick a convention and document it. If you're serious about rate limiting, you'll eventually also want 503.

That's the main four-hundred set. The two runners-up, if I had to extend to seven: 405 Method Not Allowed (URL exists but not with this HTTP method — include Allow header), and 422 Unprocessable Content (body parsed fine, but the values have semantic problems, like "email field is a valid string but not a valid email"). 422 is a better fit than 400 for most validation errors in practice.

5xx — Server error (two of them matter)

500 Internal Server Error — something broke on your end and you don't have a more specific code. Unhandled exceptions, database connection drops, any bug. Don't leak stack traces in the 500 body in production. The client can only retry, blindly.

503 Service Unavailable — you're temporarily down (maintenance, overload, circuit breaker). Include Retry-After. Pair with Cache-Control: no-cache so intermediate proxies don't serve the 503 to other clients.

502 (Bad Gateway) and 504 (Gateway Timeout) are worth recognizing when you see them — they come from load balancers and reverse proxies, not your app — but you don't typically return them from application code. They're messages about your app's failures.

The mistakes I see most often

Returning 200 with an error body. The cardinal sin. {"success": false, "error": "not found"} with HTTP 200 breaks every HTTP-aware tool: proxy retry logic, monitoring, client libraries that branch on status code, CDN cache invalidation rules. Every layer in the network now has to know your custom error convention. Use real status codes — that's what they're for.

401 vs 403 confusion. The test: would different credentials fix the problem? If yes, 401. If no (the user is already who they claim to be but still not allowed), 403. A surprising number of APIs return 403 for missing-token scenarios. It's almost always wrong. The token being missing means "unauthenticated," which is 401.

400 for everything. 400 should specifically mean "malformed request, I can't even parse it." Validation errors on well-formed JSON with an invalid email are better served by 422. Permission issues are 403. Missing resource is 404. Generic-400 for every client-side problem is lazy and it robs your clients of the ability to handle errors differently.

Missing Location headers on 201. The spec says 201 Created SHOULD include Location: /the/new/resource. Most APIs skip it. Cost: clients either have to construct the URL themselves (coupling them to your URL scheme) or re-fetch the resource from a list endpoint. Adding the header is a one-line fix that most frameworks support natively.

Inconsistent use of 404 vs 410. 404 means "I have no information about whether this ever existed." 410 means "I know it existed, and I deliberately removed it." Using 404 for deleted resources is acceptable; using 410 for resources you don't actually know the history of is not. Search engines update their indexes faster for 410, so use it deliberately for retired URLs where you want quick de-indexing.

No Retry-After on 429 or 503. Both codes explicitly exist to communicate "try again later." Without Retry-After, clients have to guess (typically retry-with-exponential-backoff), and in aggregate that causes retry storms that keep your service down longer. One header. Always send it.

308 confused with 301. Both are "permanent." 301 has historically allowed changing POST to GET; 308 is guaranteed not to. If you're redirecting an API endpoint (not a browser URL), you almost always want 308. If you're redirecting a browser URL that only handles GET anyway, 301 is fine.

What about the other 48 codes?

They have their uses. 206 matters if you're building video streaming or resumable-downloads. 409 Conflict matters if your API has optimistic concurrency semantics. 418 I'm a Teapot is a joke from RFC 2324 (1998) that somehow survived two attempts to remove it from the registry. 451 Unavailable for Legal Reasons (from Bradbury's Fahrenheit 451) matters for transparency around legally-blocked content.

For a full walkthrough with practical guidance on every code, see the HTTP Status Code Reference. Searchable, deep-linkable (/http-status/#404 jumps to 404), and organized by class.

One more principle

The HTTP spec assigns meaning to the first digit — 2xx success, 3xx redirect, 4xx client error, 5xx server error — and that meaning is more important than the specific code. A well-designed API makes sure the first digit is always correct, even if the exact code is a judgment call. 422 vs 400 is a small debate; 200-with-an-error-body vs any 4xx is a big one.

Get the class right first. Pick the specific code from this shortlist. Only reach for the long tail when you have a specific reason (WebDAV, range requests, content negotiation, TLS 0-RTT). Most APIs don't.