HTTP status codes are the language your server speaks to describe what happened with a request. Choosing the right code makes your API self-documenting — a developer reading your responses immediately understands what went wrong and what to do next. Choosing the wrong code makes debugging a guessing game.
The Categories
Status codes are grouped by their first digit:
| Range | Category | Meaning | |---|---|---| | 1xx | Informational | Request received, continuing | | 2xx | Success | Request was received, understood, accepted | | 3xx | Redirection | Further action needed | | 4xx | Client Error | Request is wrong | | 5xx | Server Error | Server failed to fulfil a valid request |
The Most Misused Codes
401 vs 403
This trips up many developers.
401 Unauthorized — "I don't know who you are." The request lacks valid authentication credentials. The client should authenticate and retry. Despite the name, it's really about authentication, not authorization.
403 Forbidden — "I know who you are, but you can't do this." The client is authenticated but doesn't have permission for the resource. No amount of re-authenticating will help — they simply don't have access.
Rule: if there's no auth token, use 401. If there is a token but the user lacks permission, use 403.
400 vs 422
400 Bad Request — the request is malformed. Invalid JSON, wrong Content-Type, missing required headers. The server can't parse or understand it at all.
422 Unprocessable Entity — the request is syntactically correct but semantically wrong. Valid JSON, but the data violates business rules: email already exists, date is in the past, quantity is negative. Return field-level validation errors in the body.
Use 422 for validation errors in REST APIs — it tells clients the format was right but the content wasn't acceptable.
301 vs 302 vs 307 vs 308
These are all redirects, but the differences matter for SEO and API design:
| Code | Type | Method preserved | SEO | |---|---|---|---| | 301 | Permanent | No (becomes GET) | ✓ passes link equity | | 302 | Temporary | No (becomes GET) | ✗ no equity transfer | | 307 | Temporary | Yes | ✗ no equity transfer | | 308 | Permanent | Yes | ✓ passes link equity |
For domain migrations and permanent URL changes, use 301. For temporary redirects (A/B testing, maintenance pages), use 302. If you're redirecting a POST form and want to preserve the method, use 307.
The Codes You Should Use More
204 No Content
Return this on successful DELETE operations or PUT/PATCH when you don't need to send back the updated resource. It explicitly tells the client "success, nothing to return" rather than sending an empty 200 body.
409 Conflict
Use this when a request conflicts with current state: duplicate username registration, concurrent edit conflicts, version mismatch in optimistic locking. Much clearer than a generic 400.
410 Gone
The resource existed but has been permanently deleted. More informative than 404 — it tells search engine crawlers to remove the URL from their index. Use it when you intentionally remove content (deleted blog posts, retired API versions).
429 Too Many Requests
Standard response for rate limiting. Always include a Retry-After header with the number of seconds to wait, or an X-RateLimit-* header family with quota information.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1700000000
5xx Codes in Production
500 Internal Server Error is the catch-all. In production, all unhandled exceptions should return 500 — never expose stack traces or internal error messages to clients.
502 Bad Gateway and 504 Gateway Timeout are gateway/proxy errors. If you're seeing these in production, your upstream service (app server, database, microservice) is down or slow — not the nginx/load balancer itself.
503 Service Unavailable should include a Retry-After header. Use it for planned maintenance windows so clients know exactly when to retry.
Designing Good Error Responses
The status code is the headline. The body is the story. A well-designed error response:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The request data is invalid.",
"details": [
{ "field": "email", "message": "Email address is already registered." },
{ "field": "age", "message": "Must be 18 or older." }
]
}
}
Include a machine-readable error code (for client-side handling), a human-readable message (for developers), and field-level details when applicable.
Quick Reference
Bookmark our HTTP Status Code Reference — every code from 100 to 504, searchable by number, name, or description, with plain-English explanations and "when to use" guidance. Filter by category to quickly find what you need.