HTTP status codes explained
A short reference on HTTP status codes — the three-digit grammar, what each class means, the twenty codes you actually meet, 401 vs 403, 301 vs 302, the 502/503/504 triage table for reverse proxies, and the retry semantics most clients get wrong.
The one-line definition
An HTTP status code is the three-digit number a server returns with every response, and it is a sentence in a tiny grammar: the first digit names who succeeded or failed, the last two name how. Learn the five classes and you can triage most web failures before reading a single log line. The current definitions live in RFC 9110; the grammar itself is older than the web, inherited from FTP-era protocols.
The five classes
| Class | Meaning | Mnemonic |
|---|---|---|
| 1xx | Informational — "keep going" | hold on |
| 2xx | Success | here you go |
| 3xx | Redirection — look elsewhere | go away (politely) |
| 4xx | Client error — your request is wrong | you messed up |
| 5xx | Server error — my handling broke | I messed up |
The 4/5 split is the load-bearing distinction: it assigns blame. A 4xx says fix the request; a 5xx says fix the server. Every debugging session starts by believing the first digit.
The codes you actually meet
| Code | Name | When you see it |
|---|---|---|
| 200 | OK | The happy path |
| 201 | Created | POST succeeded, new resource exists |
| 204 | No Content | Success with an empty body (DELETEs, OPTIONS) |
| 301 | Moved Permanently | Redirect that browsers and search engines cache |
| 302 | Found | Temporary redirect; nothing should be cached |
| 304 | Not Modified | Your cached copy is still good — the caching win |
| 400 | Bad Request | Malformed body, invalid JSON, missing field |
| 401 | Unauthorized | Misnamed: means unauthenticated — who are you? |
| 403 | Forbidden | Authenticated, but not allowed — I know you, no |
| 404 | Not Found | The famous one — nothing at this path |
| 405 | Method Not Allowed | Right path, wrong verb (POST to a GET route) |
| 409 | Conflict | State collision — duplicate create, stale update |
| 410 | Gone | Like 404, but "and it's never coming back" — deliberate |
| 418 | I'm a teapot | The 1998 April-1 coffee-pot joke that shipped; a favorite for blocking bots |
| 422 | Unprocessable Entity | Parsed fine, semantically invalid — the validation-error workhorse |
| 429 | Too Many Requests | Rate limited; honor Retry-After |
| 500 | Internal Server Error | Unhandled exception, the generic server crash |
| 502 | Bad Gateway | Proxy reached upstream, got garbage or a refusal |
| 503 | Service Unavailable | Server up but not serving — overloaded, draining, booting |
| 504 | Gateway Timeout | Proxy reached upstream, got silence until the deadline |
The pairs people mix up
401 vs 403 — 401 means authenticate (you haven't proven who you are; send credentials); 403 means authorization failed (identity fine, permission denied). If retrying with a login could help: 401. If no login will ever help: 403.
301 vs 302 — permanence is a promise to caches. A 301 gets remembered by browsers aggressively and transfers SEO authority; a mistaken 301 haunts users until their cache dies. When unsure, 302 is the reversible mistake.
404 vs 410 — 404 is "nothing here (right now, who knows)"; 410 is an affidavit that the thing existed and was removed. Crawlers deindex 410s faster.
The reverse-proxy triage table
Behind a reverse proxy like Caddy, the 50x triplet tells you exactly where to look:
- 502 — proxy reached the app and got a broken answer: app crashed mid-response, wrong port, TLS1 mismatch. Look at the app's logs.
- 503 — someone is deliberately not serving: app still starting, health check failing, maintenance mode. Look at readiness.
- 504 — the app never answered: hung request, dead upstream host, firewall eating packets. Look at timeouts and connectivity.
🔗 Learn more — 1 What is TLS (and how does Let's Encrypt fit)?
Memorize the triplet and "the site is down" becomes a routing decision.
The footguns
200 with an error body. APIs that return 200 {"error": ...} defeat every piece of generic tooling — monitors, retriers, caches all believe the first digit. The status line is the contract; use it.
Retry semantics. 429 and 503 are the invitations to retry (with backoff, honoring Retry-After). Blind retries on 4xx are a bug: the request was wrong once, it will be wrong again — and retried non-idempotent2 POSTs are how double charges happen.
🔗 Learn more — 2 What is idempotency (in data pipelines)?
Custom codes. Inventing 599 or 250 technically works and breaks intermediaries subtly. The registry exists; 422/409/503 cover nearly every "custom" need.