HTTP status codes: the only reference you'll need
Every HTTP status code with plain-English meaning — when to send which, when to care about the distinction, and the codes you can usually ignore.
# The five classes
- 1xx — Informational (rare, you can ignore most of them)
- 2xx — Success
- 3xx — Redirect
- 4xx — Client error (something wrong with the request)
- 5xx — Server error (something wrong on our end)
# 1xx — You probably won't send these
| Code | Name | Use |
|------|---------------------|---------------------------------------------------------|
| 100 | Continue | "Send the body." Mostly invisible, handled by HTTP libs.|
| 101 | Switching Protocols | WebSocket upgrade handshake. |
| 102 | Processing | WebDAV; long operations. |
| 103 | Early Hints | New-ish. Pre-emptively hint at <link rel="preload"> before the full response. Worth using.|
# 2xx — It worked
| Code | Name | Use |
|------|---------------------|---------------------------------------------------------|
| 200 | OK | Default success. |
| 201 | Created | POST that created a new resource. Include a Location: header. |
| 202 | Accepted | Async job queued. The work isn't done yet. |
| 204 | No Content | Success with empty body. Great for DELETE. |
| 206 | Partial Content | Response to a Range: request (video streaming, resumable downloads).|
# 3xx — Go look somewhere else
| Code | Name | Use |
|------|---------------------|---------------------------------------------------------|
| 301 | Moved Permanently | Permanent URL change. Browsers + SEO cache aggressively.|
| 302 | Found | Temporary redirect. Same HTTP method on the redirect. |
| 303 | See Other | After a POST, redirect to a GET. Rarely used directly. |
| 304 | Not Modified | Response to a conditional GET. The browser already has a fresh cached copy.|
| 307 | Temporary Redirect | Like 302 but guarantees same method (POST stays POST). |
| 308 | Permanent Redirect | Like 301 but guarantees same method. Use this for route migrations.|
<div class="callout callout-tip" role="note"><div class="callout-title">Tip</div><div class="callout-body"><p>For slug renames on a content site, prefer <strong>308 over 301</strong> — 308 preserves the HTTP method on the redirect, so POSTs don't silently become GETs.</p></div></div>
# 4xx — You broke it
| Code | Name | Use |
|------|-------------------------|----------------------------------------------------|
| 400 | Bad Request | Malformed syntax — invalid JSON, missing field, bad encoding.|
| 401 | Unauthorized | Needs auth. Include WWW-Authenticate: header. |
| 402 | Payment Required | Reserved but rarely used. Stripe uses it. |
| 403 | Forbidden | Authenticated but not allowed. |
| 404 | Not Found | Resource doesn't exist. |
| 405 | Method Not Allowed | Resource exists but doesn't support this verb. Include Allow: header.|
| 406 | Not Acceptable | Can't satisfy Accept: header. |
| 409 | Conflict | Edit conflict, duplicate username, etc. |
| 410 | Gone | Permanently removed. Stronger than 404 for SEO. |
| 412 | Precondition Failed | If-Match / If-Unmodified-Since didn't match. |
| 413 | Payload Too Large | Body exceeded server limits. |
| 414 | URI Too Long | Usually means query-string overflow. |
| 415 | Unsupported Media Type | Wrong Content-Type: (e.g. sent XML, server wants JSON). |
| 418 | I'm a Teapot | Joke code, but it's in real RFCs. Rarely used. |
| 422 | Unprocessable Entity | Syntactically valid but semantically wrong — validation failures. Preferred by REST APIs over 400.|
| 429 | Too Many Requests | Rate limited. Include Retry-After: header. |
<div class="callout callout-note" role="note"><div class="callout-title">Note</div><div class="callout-body"><p><strong>400 vs 422</strong>: 400 is for syntactic errors (can't parse JSON). 422 is for semantic errors (JSON parsed, but <code>email: "nope"</code> isn't a valid email). Both are correct; 422 is more specific.</p></div></div>
# 5xx — We broke it
| Code | Name | Use |
|------|-------------------------|----------------------------------------------------|
| 500 | Internal Server Error | Uncaught exception. Should never be the intended response.|
| 501 | Not Implemented | Method the server doesn't support at all. |
| 502 | Bad Gateway | Upstream server returned garbage. |
| 503 | Service Unavailable | Overloaded or down for maintenance. Include Retry-After: header.|
| 504 | Gateway Timeout | Upstream timed out. |
| 507 | Insufficient Storage | Disk full. Real problem. |
# The codes you'll actually send
For a typical REST API, these cover ~95% of responses:
- 200 — GET/PUT/PATCH success
- 201 — POST created something
- 204 — DELETE success (no body)
- 301 / 308 — permanent URL moves
- 400 — malformed request (bad JSON)
- 401 — not logged in
- 403 — logged in, not allowed
- 404 — resource doesn't exist
- 409 — conflict (duplicate, stale write)
- 422 — validation error
- 429 — rate limited
- 500 — we crashed
- 503 — we're overloaded / down
Everything else is specialised.
# Common mistakes
- Returning
200 OKwith{"error": "..."}for a REST API. Your monitoring can't tell success from failure without parsing bodies. - Using 301 for temporary redirects. The browser caches it "forever" — you're stuck with it.
- Using 401 when you mean 403. Auth header won't fix a permissions problem.
- 5xx for client bugs. If the user sent bad input, it's a 4xx.
- Ignoring
Retry-Afteron 429 and 503. Libraries and CDNs honour it; hand-rolled retry loops often don't.
# Related tools
- URL Encode / Decode — for query strings that trigger 414s
- JWT Decoder — for debugging 401s with bearer tokens
Frequently asked questions
›What's the difference between 401 and 403?
401 means 'I don't know who you are' (send credentials). 403 means 'I know who you are and you're not allowed' (credentials won't help). The mnemonic: 401 = unauthenticated, 403 = unauthorized.
›Should I use 410 or 404 for deleted resources?
404 if you don't want to say whether it ever existed. 410 if you want to signal permanent removal — Google actually treats 410 as a stronger 'stop indexing' signal than 404 for SEO.
›Is it okay to return 200 with `{error: '...'}`?
GraphQL does this by design. For REST, it's considered bad form — you're hiding failures from HTTP-level monitoring, CDNs, and retry logic. Return the actual status code and put the error detail in the body.