HTTP Status Codes — Choosing 200, 404, 500 and Friends
Status codes are a shared language between clients, proxies, and humans reading logs. Misusing them — 200 with an error body, 500 for bad input — breaks retries, caching, and monitoring. This reference focuses on codes you will actually pick while building web APIs and sites.
Quick map
| Class | Meaning | Client heuristic |
|---|---|---|
| 1xx | Informational | Rare in app code |
| 2xx | Success | Proceed |
| 3xx | Redirect | Follow or fail closed |
| 4xx | Client error | Fix request; careful retries |
| 5xx | Server error | Retry with backoff if idempotent |
2xx — success without lying
| Code | Use when |
|---|---|
| 200 OK | Successful GET/PUT/PATCH with body |
| 201 Created | Resource created; prefer Location header |
| 202 Accepted | Async work queued; not done yet |
| 204 No Content | Success with empty body (DELETE often) |
Prefer 201 over 200 for creates so clients can branch cleanly. Do not return 200 with { "error": "..." } — that hides failures from CDNs and generic HTTP clients.
3xx — redirects and caches
| Code | Notes |
|---|---|
| 301 | Permanent redirect; caches remember |
| 302/307 | Temporary; method handling differs (307 preserves method) |
| 304 | Not Modified — conditional GET success |
| 308 | Permanent; preserve method |
For APIs, avoid surprise redirects on POST. For websites, be consistent about www/apex and trailing slashes so you do not invent redirect loops.
4xx — the client did something wrong (usually)
| Code | Use when |
|---|---|
| 400 Bad Request | Malformed JSON, failed validation (generic) |
| 401 Unauthorized | Missing/invalid authentication |
| 403 Forbidden | Authenticated but not allowed |
| 404 Not Found | No resource (or hide existence on purpose) |
| 405 Method Not Allowed | Wrong verb; include Allow |
| 408 Request Timeout | Client took too long |
| 409 Conflict | Version conflict, duplicate unique key |
| 410 Gone | Used to exist; intentional removal |
| 415 Unsupported Media Type | Wrong Content-Type |
| 422 Unprocessable Entity | Semantically invalid (popular in APIs) |
| 429 Too Many Requests | Rate limit; send Retry-After |
401 vs 403: unauthenticated vs unauthorized. Browsers treat them differently for UX; APIs should stay precise.
404 vs 403: some security teams return 404 for other users’ private ids to avoid oracle leaks — document the choice.
5xx — your problem (or your dependency’s)
| Code | Use when |
|---|---|
| 500 | Unexpected bug; log correlation id |
| 502 Bad Gateway | Upstream returned invalid response |
| 503 Service Unavailable | Overload/maintenance; Retry-After helps |
| 504 Gateway Timeout | Upstream too slow |
Map dependency failures carefully: if Postgres is down mid-request, 503 may be more honest than 500 for load balancers. Include an opaque request_id in the JSON body for support.
Caching and intermediaries
200/301/404 can be cached depending on headers. Set-Control and Vary matter. Returning 404 for “user not found” on a highly cacheable public CDN URL can sticky-cache a miss — choose cache policies deliberately for APIs.
Idempotency and retries
Clients safely retry GET and often PUT/DELETE. Blindly retrying POST after a timeout can double-charge — use idempotency keys and make status codes reflect whether creation happened (200/201 vs 409).
Problem details bodies
RFC 7807 application/problem+json pairs well with 4xx/5xx:
{
"type": "https://example.com/errors/validation",
"title": "Invalid email",
"status": 422,
"detail": "email must be a valid address",
"instance": "/v1/users"
}
Keep status in sync with the HTTP line.
Anti-patterns to delete
- Everything is
200 - Validation failures as
500 401when the password is wrong and when the account is banned (collapse only if intentional)- HTML error pages for JSON API clients without content negotiation
Bottom line
Pick the narrowest honest code: 201 for creates, 401/403 for authZ, 404/409/422 for client mistakes, 503 for capacity, 500 for truly unexpected bugs. Your future on-call graphs — and every HTTP client library — depend on that honesty.