Picking HTTP status codes without flinching
HTTP defines hundreds of status codes, but everyday API work uses maybe twenty. The hard part is the choice — “200 or 201?” “401 or 403?” — and consistency here directly affects API quality. This article focuses on the codes that cause the most disagreement.
Class-level meanings, taken seriously
Status codes split into five classes by their first digit. Reading the spec text (RFC 9110) carefully is the single best step toward less confusion.
| Class | Name | Meaning |
|---|---|---|
| 1xx | Informational | Provisional response |
| 2xx | Successful | Request was accepted and processed |
| 3xx | Redirection | Client must take another step |
| 4xx | Client Error | Server refused due to a client mistake |
| 5xx | Server Error | Server failed to fulfill the request |
The line between 4xx and 5xx is critical for operations: it determines whether a failure should page on-call or not.
2xx variants
| Code | Use |
|---|---|
| 200 OK | Generic success |
| 201 Created | New resource created (typical POST result) |
| 202 Accepted | Accepted; processing continues asynchronously |
| 204 No Content | Success with empty body (typical DELETE result) |
201 vs 200
For “this POST created a new resource”, the REST convention is 201 with a Location header pointing to the new resource. Many APIs settle for 200; if you want CRUD semantics to be exact, use 201.
When 202 fits
“Accepted the email, will actually send it from a job” is a clean case for 202. Returning a status URL in the body lets the client poll.
4xx: three tricky pairs
401 Unauthorized vs 403 Forbidden
The most misused pair. They feel like synonyms.
- 401 Unauthorized — no/invalid credentials. “I don’t know who you are.”
- 403 Forbidden — credentials are valid but lack permission. “I know who you are, and you can’t see this.”
Decision flow:
- Are credentials valid? If not → 401
- They are. Are they authorized for this resource? If not → 403
In API gateway logs, 401 spikes mean tokens are expiring; 403 spikes mean permissions are misconfigured. The ability to triage that way only works if the codes are used correctly.
404 Not Found vs 410 Gone
- 404 Not Found — could not be found (reason unspecified).
- 410 Gone — used to exist, intentionally removed.
Most cases call for 404. Search engines like Google drop 410 URLs from their index immediately, while 404 URLs stay in the crawl queue under “maybe it’ll come back”. For deliberately removed pages, 410 is cleaner from an SEO perspective.
400 Bad Request vs 422 Unprocessable Entity
- 400 Bad Request — the request itself is malformed (JSON parse error, missing required header).
- 422 Unprocessable Entity — the request is well-formed but content validation fails.
Many APIs collapse both into 400, and that’s fine. Splitting them helps client retry logic:
- 400 → retry won’t help.
- 422 → fix the input and retry.
This split makes error handling more legible.
429 Too Many Requests
Rate limiting. The convention is to include Retry-After so the client can wait the right amount of time:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{"error": "rate limited", "retry_after_seconds": 60} A client honoring Retry-After and adding exponential backoff is friendly to the server.
5xx variants
| Code | Use |
|---|---|
| 500 Internal Server Error | Generic unexpected server error |
| 502 Bad Gateway | Bad response from upstream / proxied service |
| 503 Service Unavailable | Temporary unavailability (maintenance, overload) |
| 504 Gateway Timeout | Upstream timed out |
When to actually return 500
Reflexive “never let 500 escape” thinking leads to swallowing exceptions and returning 200 with {success: false}. Bugs and unexpected errors should surface as 500:
- Logs surface 500 to alerting.
- Hiding errors as 200 makes them invisible.
When user experience requires returning 200 (e.g. graceful UI fallback), still log the underlying error at the 500 severity level.
503 with Retry-After
Maintenance windows are good cases for 503 + Retry-After, so smart clients can wait:
HTTP/1.1 503 Service Unavailable
Retry-After: Sat, 26 Apr 2026 12:00:00 GMT Status codes and idempotency
REST design pairs status codes with HTTP method idempotency:
- GET / PUT / DELETE are idempotent — same request multiple times yields the same result.
- POST is not — each call creates something new.
Idempotent operations should land on 200 (success) or 204 (delete success), and repeated calls should produce the same response.
For non-idempotent POSTs that hit “this already exists”, 409 Conflict lets you signal the duplication explicitly.
A decision tree for the hesitant
Did the operation succeed?
├── Yes
│ ├── New resource created? → 201
│ ├── Async processing started? → 202
│ ├── No body to return? → 204
│ └── Otherwise → 200
└── No
├── Server-side problem?
│ ├── Temporary (maintenance, overload)? → 503
│ ├── Upstream issue? → 502 / 504
│ └── Unexpected bug → 500
└── Client-side problem?
├── Missing/invalid credentials → 401
├── Insufficient permissions → 403
├── Resource doesn't exist → 404 or 410
├── Rate limited → 429
├── Malformed request → 400
└── Validation failure → 422 Posting this on the wall during API reviews keeps the conversation focused.
For looking up specific codes — including the obscure ones (418 I'm a teapot, 451 Unavailable For Legal Reasons, …) — the HTTP status code list on this site is a quick reverse reference.