Status codes
The status code is the part of your response that infrastructure you do not control acts on — caches, retry policies, load balancers, client libraries, monitoring. Getting it wrong is not a style problem; it changes behaviour several layers away.
The classes, and what each means to a client
| Class | Client should |
|---|---|
| 1xx | keep going, provisional |
| 2xx | accept the result |
| 3xx | look elsewhere |
| 4xx | not retry — fix the request |
| 5xx | retry, possibly later |
That 4xx/5xx line is the one that matters operationally. A retry policy keys on it, an alert threshold keys on it, and a 500 returned for a bad input wakes someone up for a client’s typo.
The pairs that get confused
400 or 422. 400 means the server could not parse the request at all — malformed JSON, a missing required header. 422 means it parsed fine and failed semantic validation.
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{"type": "https://example.com/probs/validation",
"title": "Validation failed",
"status": 422,
"errors": [{"field": "email", "detail": "not a valid address"}]}FastAPI returns 422 for every Pydantic failure, which is why it is so common in
Python APIs. application/problem+json is RFC 9457 (which obsoleted 7807) —
one error shape across the whole API, machine-readable.
401 or 403. 401 is unauthenticated — I do not know who you are, and it
must carry a WWW-Authenticate header. 403 is authenticated but not
permitted. Returning 403 for a missing token is a common mix-up that sends
clients hunting for a permissions problem that does not exist.
if token is None:
raise HTTPException(401, headers={"WWW-Authenticate": "Bearer"})
if not user.can(action):
raise HTTPException(403)Gotcha: for a resource the caller may not even know exists, 404 is often the right answer instead of 403. A 403 confirms the resource is there, which is an information leak in a multi-tenant system.
200 or 201 or 204. A create returns 201 with a Location header so the
client learns the new URI. A successful call with nothing to say returns 204 and
no body. 200 with an empty object is neither.
HTTP/1.1 201 Created
Location: /orders/9c3fThe ones worth knowing beyond the basics
| Code | Meaning | Why it matters |
|---|---|---|
| 409 | conflict with current state | duplicate create, version clash |
| 410 | gone, permanently | tells crawlers to forget, unlike 404 |
| 412 | precondition failed | optimistic concurrency via If-Match |
| 428 | precondition required | force clients to send If-Match |
| 429 | rate limited | must carry Retry-After |
| 503 | unavailable | deliberate shedding; carry Retry-After |
| 504 | upstream timed out | distinct from your own 500 |
429 and 503 without Retry-After are the missed opportunity: a well-behaved
client will back off correctly if you tell it how long, and will guess badly if
you do not.
The anti-pattern
HTTP/1.1 200 OK
{"success": false, "error": "insufficient funds"}Every cache stores it, every retry policy treats it as success, every dashboard
counts it as healthy, and every generic HTTP client’s raise_for_status() does
nothing. The error is invisible to everything except code that specifically
looks for it.
Interview angle 5
- “400 or 422?” - 400 for a request the server cannot parse; 422 for well-formed syntax that fails semantic validation. FastAPI returns 422 for Pydantic failures, which is why it appears so often in Python APIs.
- “401 or 403?” - 401 means unauthenticated and must carry
WWW-Authenticate; 403 means authenticated but not permitted. For a resource in another tenant, 404 is often better than 403, because 403 confirms it exists. - “What should a create return?” - 201 with a
Locationheader. 409 for a conflict with existing state, 422 for validation. Returning 200 with an error body is the anti-pattern, because clients, caches and monitoring all read it as success. - “Which 5xx codes matter operationally?” - 503 with
Retry-Afterfor deliberate load shedding and 504 for an upstream timeout. Both tell a client to retry and roughly when; a bare 500 tells it nothing and invites an immediate retry. - “Why does the exact code matter if the body explains it?” - because infrastructure you do not control reads only the code. Retry policies, caches, load balancers and alerting never look at the body.