Practical guides · · 15 min read

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.

By The Toolsy team

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

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:

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:

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:

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:

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

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":

SituationCode
GET returning data200
POST creating resource201 + Location
DELETE / no-body actions204
Permanent URL change301 or 308
Temporary URL change307
Malformed JSON / invalid syntax400
Missing or invalid auth401 + WWW-Authenticate
Authenticated but not allowed403
Resource not found404
Wrong HTTP method405 + Allow
State conflict (duplicate, etc.)409
Permanently removed resource410
Validation failed422
Rate limited429 + Retry-After
Unhandled server exception500
Down for maintenance503 + 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.

Found this useful? Share it with a developer who'd want to read it. Have a topic to suggest? Email hello@toolsy.website.

← More posts