Picking HTTP status codes without flinching

5 min read

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.

ClassNameMeaning
1xxInformationalProvisional response
2xxSuccessfulRequest was accepted and processed
3xxRedirectionClient must take another step
4xxClient ErrorServer refused due to a client mistake
5xxServer ErrorServer 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

CodeUse
200 OKGeneric success
201 CreatedNew resource created (typical POST result)
202 AcceptedAccepted; processing continues asynchronously
204 No ContentSuccess 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:

  1. Are credentials valid? If not → 401
  2. 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

CodeUse
500 Internal Server ErrorGeneric unexpected server error
502 Bad GatewayBad response from upstream / proxied service
503 Service UnavailableTemporary unavailability (maintenance, overload)
504 Gateway TimeoutUpstream 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.