HTTP status codes: which one to use, with examples
A working guide to which HTTP status code to return in which situation — the 25 codes you'll actually use, when to use 401 vs 403, when to use 409 vs 422, and the error-response patterns worth following.
HTTP status codes are one of those topics where the official RFC is short, the codes themselves are well-defined, and yet production APIs misuse them constantly. Half the responses returning 200 OK should be 4xx; half the 500s should be 4xx; half the 404s should be 401s; and almost nobody distinguishes between 401 and 403 correctly.
This post is a working guide to which status code to use in which situation, with examples. Not the full IANA list — just the ~25 codes you'll actually use, and what each one is for.
The categories at a glance
- 1xx — informational. Rare.
100 Continueappears in upload protocols;101 Switching Protocolsfor WebSocket upgrades. You almost never set these manually. - 2xx — success. The request worked.
200,201,204are the workhorses. - 3xx — redirection. Resource is somewhere else, or the client should check its cache.
301,302,304,307,308have meaningful distinctions. - 4xx — client error. The request was wrong.
400,401,403,404,409,422,429each mean something different. - 5xx — server error. Something on your side broke.
500,502,503,504point at different parts of the stack.
2xx: success, but specifically which kind
200 OK — the default success
Use for any successful request that returns a body with the requested data. GET /users/42 returns 200 with the user object. POST /search returns 200 with results. Most successful responses are 200.
201 Created — for resource creation
Use specifically when a POST creates a new resource. The response body should include the created resource, and the Location header should point at it:
POST /api/users
Content-Type: application/json
{ "name": "Alice" }
→ 201 Created
Location: /api/users/42
Content-Type: application/json
{ "id": 42, "name": "Alice", "createdAt": "2026-05-20T10:00:00Z" }
If your POST didn't create anything (it was an action, not a creation), use 200 or 204 instead.
204 No Content — successful, nothing to say
The request worked and there's no body to return. Use for: DELETE requests, PUT requests that don't return the modified resource, action endpoints like POST /api/users/42/logout where the response is just "done".
204 means no body, period. Don't return a response body with 204 — some clients will reject the response or treat it as a parsing error. If you want to return data, use 200.
206 Partial Content — for range requests
Used when a client requests a byte range of a resource (typically for video streaming, downloads, or resumable transfers). Response includes Content-Range header. You don't write 206 manually — your web server emits it when handling Range: requests.
3xx: redirects done right
301 Moved Permanently
The resource has moved to a new URL forever. Browsers and search engines will update their bookmarks/indexes. Cached aggressively by default.
Use for: site migrations (old URL → new URL), removing a www subdomain, switching from HTTP to HTTPS at the application level. Don't use 301 for temporary moves — the aggressive caching means clients won't check back.
302 Found and 307 Temporary Redirect
Both mean "the resource is temporarily at another URL". The difference is subtle:
- 302 — clients are allowed to change
POSTtoGETwhen following. Historical quirk; in practice most clients do change. - 307 — clients must not change the method. A 307 on
POSTmeans re-POST to the new location.
Use 307 when you mean it. Use 302 only when you want the (historical, often unintended) GET-conversion behavior.
308 Permanent Redirect
Like 301 but preserves the method. 301 has the same historical quirk as 302 (some clients change POST to GET); 308 is the unambiguous "permanent, keep the method".
Practical rule: 301/308 for permanent, 307 for temporary, only use 302 when you specifically want method-conversion.
304 Not Modified
The cached version the client has is still fresh — they shouldn't request the body. Used in response to If-None-Match (with ETag) or If-Modified-Since (with Last-Modified) conditional requests. Saves bandwidth.
You don't usually set 304 manually — your framework or CDN handles ETag and conditional-request logic.
4xx: distinguishing types of client error
400 Bad Request — request was malformed
The request couldn't be parsed or violates the API contract. Malformed JSON, missing required fields, invalid field types, query parameters that don't make sense together.
Don't use 400 for "we received valid input but it failed business logic" (use 409 or 422 for that — covered below). 400 is for syntactically invalid requests.
401 Unauthorized — you need to authenticate
The request lacks valid authentication credentials. Use when:
- No
Authorizationheader on an endpoint that requires one - The token/cookie is expired
- The provided credentials are wrong
401 responses MUST include a WWW-Authenticate header indicating how to authenticate:
401 Unauthorized WWW-Authenticate: Bearer realm="api"
The name "401 Unauthorized" is historically confusing — it actually means "unauthenticated". The distinction matters for the next code.
403 Forbidden — you're authenticated but not allowed
The server identified you (auth succeeded) but you don't have permission for this resource. Different role required, accessing someone else's data, organization-level restriction, etc.
The distinction between 401 and 403:
- Got 401? Try authenticating (or re-authenticating).
- Got 403? You're authenticated; this resource isn't for you. Re-authenticating won't help.
A common bug: returning 401 for "you're logged in but can't access this resource". The client retries auth, gets 401 again, and concludes auth is broken. Should be 403.
404 Not Found
The requested resource doesn't exist. Use for: a URL with no matching route, a resource by ID that doesn't exist, an endpoint that's been removed.
Security note: Some teams use 404 instead of 403 to avoid revealing the existence of resources you don't have access to. "Does the user admin@company.com exist?" — returning 403 says yes; returning 404 hides the information. Tradeoff: harder to debug legitimate access issues.
405 Method Not Allowed
The URL exists but doesn't accept the HTTP method you used. GET /api/users works, but DELETE /api/users doesn't. Response must include an Allow header listing valid methods:
405 Method Not Allowed Allow: GET, POST
409 Conflict — state-based rejection
The request was syntactically valid but conflicts with the current state of the resource. Examples: trying to create a user with an email that already exists, updating a record that someone else just modified (with If-Match failing), deleting a resource that has dependents.
Distinguished from 400 by being about state, not format.
410 Gone — permanently deleted
The resource used to exist and is now permanently removed. Different from 404 (which might just be "we couldn't find it"); 410 is explicit "this is gone forever, don't ask again".
Useful for deprecation: when you remove an endpoint, return 410 with a body explaining the replacement. Crawlers will deindex 410 URLs faster than 404s.
422 Unprocessable Entity — semantically invalid
The request was syntactically valid (parsed correctly) but failed semantic validation. Examples: email field contains a non-email string, age field is negative, end-date is before start-date.
Distinguished from 400 (which is about format) and 409 (which is about state). 422 is the right code for "your fields make sense in isolation but the values are wrong":
POST /api/users
{ "email": "not-an-email", "age": -5 }
→ 422 Unprocessable Entity
{
"errors": [
{ "field": "email", "message": "must be a valid email" },
{ "field": "age", "message": "must be positive" }
]
}
429 Too Many Requests — rate limited
The client has sent too many requests in a window. Response SHOULD include a Retry-After header (either a seconds count or an HTTP-date):
429 Too Many Requests Retry-After: 60
Many APIs also include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers so clients can pace themselves before hitting the limit.
5xx: when something on your side is broken
500 Internal Server Error
The server encountered an error it couldn't classify. Used when your code throws an unhandled exception. 500 is your fault, not the client's. A 500 means your application crashed; the client did nothing wrong.
Don't return 500 for things that should be 4xx. "User entered a bad email" → 422. "Authentication required" → 401. 500s should be rare; their rate is a key reliability metric.
502 Bad Gateway and 504 Gateway Timeout
Used by intermediaries (load balancers, reverse proxies) when the upstream server is broken. 502 means the upstream returned something invalid; 504 means it didn't respond in time.
Your application code rarely emits these — they come from nginx, Cloudflare, AWS ALB, etc. when your app server is down or unresponsive.
503 Service Unavailable
The server is temporarily unable to handle requests. Used for: scheduled maintenance windows, when a critical dependency (database) is down, or when a circuit breaker has tripped.
Response SHOULD include Retry-After indicating when to try again. 503 with no Retry-After tells the client to assume "try again soon" — which they will, sometimes aggressively.
Headers that every 4xx/5xx response should have
Error responses are still HTTP responses — they should be helpful, not just terse. Conventions:
Content-Type: application/problem+json— the RFC 7807 standard for machine-readable error responses. Body format includestype,title,status,detail, and optionalinstance.- An error code in the body. A short string the client can pattern-match on (
"insufficient_balance","email_already_in_use"). HTTP statuses are coarse; codes let you distinguish business errors at the same status. - A human-readable message. What went wrong, in plain English. The message should be safe to show to users.
- A correlation ID. Either echoed from the request's
X-Request-Idor generated server-side. Lets users include it in support tickets so you can find the relevant logs.
A complete error response:
422 Unprocessable Entity
Content-Type: application/problem+json
X-Request-Id: 5b7e3f9c-...
{
"type": "https://api.example.com/errors/validation",
"title": "Validation failed",
"status": 422,
"code": "validation_error",
"detail": "The request failed semantic validation",
"errors": [
{ "field": "email", "message": "must be a valid email" }
],
"instance": "/api/users",
"request_id": "5b7e3f9c-..."
}
The codes I rarely use but you should know exist
- 206 — partial content (range requests)
- 207 — multi-status (WebDAV; rare outside that)
- 418 — I'm a teapot (joke, RFC 2324; some APIs use it for "request blocked by anti-abuse")
- 451 — unavailable for legal reasons (named after Fahrenheit 451; used when content is removed due to legal demands)
- 426 — upgrade required (used to tell clients to upgrade TLS or HTTP version)
You can browse all HTTP status codes with detailed explanations in our HTTP status code reference tool — including the rare ones not covered here.
The cheat sheet
Quick reference for "which code do I return":
| Situation | Code |
|---|---|
| GET returning data | 200 |
| POST creating resource | 201 + Location |
| DELETE / no-body actions | 204 |
| Permanent URL change | 301 or 308 |
| Temporary URL change | 307 |
| Malformed JSON / invalid syntax | 400 |
| Missing or invalid auth | 401 + WWW-Authenticate |
| Authenticated but not allowed | 403 |
| Resource not found | 404 |
| Wrong HTTP method | 405 + Allow |
| State conflict (duplicate, etc.) | 409 |
| Permanently removed resource | 410 |
| Validation failed | 422 |
| Rate limited | 429 + Retry-After |
| Unhandled server exception | 500 |
| Down for maintenance | 503 + Retry-After |
If you're returning 200 for everything (even errors), with a JSON body indicating success or failure — that's a common anti-pattern. HTTP has a perfectly good error-signaling layer; use it. Tools like API monitors, retry libraries, and ops dashboards all use status codes; returning 200 for errors makes them invisible.