Your API returns a 200 with { "success": false, "error": "Not found" } inside the body. Your frontend treats every non-200 as a network failure. Your load balancer returns a 502 and nobody knows if the problem is the proxy, the app, or the database.
HTTP status codes are the common language between clients and servers, and most developers only know about ten of them. This guide covers the ones you'll actually encounter, what they mean, when to use them in your own APIs, and the debugging patterns for when things go wrong.
The Five Classes
HTTP status codes are three-digit numbers grouped by their first digit:
| Range | Class | Meaning |
|---|---|---|
| 1xx | Informational | Request received, continuing process |
| 2xx | Success | Request received, understood, and accepted |
| 3xx | Redirection | Further action needed to complete the request |
| 4xx | Client Error | Request contains bad syntax or can't be fulfilled |
| 5xx | Server Error | Server failed to fulfill a valid request |
The distinction between 4xx and 5xx is critical: 4xx means the client did something wrong (bad input, missing auth, resource doesn't exist), and 5xx means the server did something wrong (crashed, timed out, misconfigured). This distinction drives retry logic, alerting, and error handling throughout your entire stack.
The Status Codes You'll Actually Use
2xx — Success
200 OK — The request succeeded. The most common response. The meaning depends on the HTTP method: GET returns the resource, POST returns the result of the action, DELETE confirms the deletion.
201 Created — A new resource was created. Use this for POST requests that create something. Include a Location header pointing to the new resource:
HTTP/1.1 201 Created
Location: /api/users/42
Content-Type: application/json
{ "id": 42, "name": "Jane Doe" }
204 No Content — Success, but there's nothing to return. The correct response for a successful DELETE or a PUT that doesn't need to return the updated resource. The response body must be empty.
206 Partial Content — The server is returning part of a resource. Used with Range headers for video streaming, resumable downloads, and large file transfers. If you've ever paused and resumed a download, 206 is what made that work.
3xx — Redirection
301 Moved Permanently — The resource has permanently moved to a new URL. Search engines transfer SEO authority to the new URL. Browsers cache this aggressively.
HTTP/1.1 301 Moved Permanently
Location: https://newdomain.com/page
302 Found — Temporary redirect. The resource is temporarily at a different URL. Search engines keep the original URL indexed. Use this for maintenance pages, A/B testing redirects, or temporary URL shorteners.
304 Not Modified — The resource hasn't changed since the client last fetched it. The server sends this instead of the full response body when the client includes If-None-Match (ETag) or If-Modified-Since headers. This is how browser caching works.
Client: GET /api/data (If-None-Match: "abc123")
Server: 304 Not Modified (no body — use your cached version)
307 Temporary Redirect — Like 302, but guarantees the HTTP method doesn't change. A POST to URL A that returns 307 means "POST to URL B instead." With 302, some clients change POST to GET (a legacy quirk).
308 Permanent Redirect — Like 301, but preserves the HTTP method. Use this for API versioning redirects where you want POST requests to stay as POST.
4xx — Client Error
400 Bad Request — The request is malformed. Missing required fields, invalid JSON, wrong data types. Your API should return a helpful error message:
{
"error": "validation_error",
"message": "The 'email' field must be a valid email address",
"field": "email"
}
401 Unauthorized — Authentication is required and has either failed or not been provided. The name is misleading — it's about authentication (who are you?), not authorization (what can you do?). Should include a WWW-Authenticate header.
403 Forbidden — The server understood the request but refuses to authorize it. Unlike 401, re-authenticating won't help — this user simply doesn't have permission. Example: a regular user trying to access the admin panel.
The difference matters:
- 401: "I don't know who you are. Please log in."
- 403: "I know who you are. You're not allowed."
404 Not Found — The resource doesn't exist. The most recognized status code on the internet. Also used to hide resources — returning 404 instead of 403 when you don't want to reveal that a resource exists at all.
405 Method Not Allowed — The HTTP method isn't supported for this URL. Example: sending a DELETE to an endpoint that only accepts GET and POST. Should include an Allow header listing valid methods.
409 Conflict — The request conflicts with the current state of the resource. Common in APIs: creating a user with an email that already exists, or updating a resource that was modified by someone else since you last fetched it (optimistic locking).
{
"error": "conflict",
"message": "A user with this email already exists"
}
422 Unprocessable Entity — The request syntax is correct (it's valid JSON) but the content is semantically wrong (the values don't make sense). Some teams use 400 for everything; others distinguish between malformed requests (400) and invalid-but-well-formed requests (422).
429 Too Many Requests — Rate limiting. The client has sent too many requests in a given time window. Include Retry-After header to tell the client when to try again:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706000000
5xx — Server Error
500 Internal Server Error — The catch-all server error. An unhandled exception, a null pointer, a database query failure. If you're seeing 500s in production, something in your code is crashing and your error monitoring should be screaming.
502 Bad Gateway — The server, acting as a gateway or proxy, received an invalid response from the upstream server. In practice: your Nginx/load balancer can reach itself fine, but the application server behind it is returning garbage or not responding properly.
503 Service Unavailable — The server is temporarily unable to handle the request. Usually means the server is overloaded or under maintenance. Include a Retry-After header. Load balancers return this when all backend instances are unhealthy.
504 Gateway Timeout — The proxy/gateway didn't get a response from the upstream server in time. Your application is too slow for the proxy's timeout. Either the request is taking too long, or the app has frozen (deadlock, infinite loop, resource exhaustion).
Debugging with Status Codes
The 502/503/504 Triangle
These three cause the most confusion in production:
Client → Load Balancer → App Server → Database
- 502: Load balancer reached the app server, got a bad response (app crashed mid-response)
- 503: Load balancer knows the app server is down (health check failed, no instances available)
- 504: Load balancer reached the app server, but it took too long to respond (slow query, deadlock)
Debugging steps:
- Check app server logs — is the process running?
- Check load balancer logs — what upstream error is it seeing?
- Check resource utilization — CPU, memory, disk, database connections
- Check network — can the load balancer actually reach the app server?
The 401/403 Confusion
When users report "access denied," you need to know which one:
- If 401: Check if the token is expired, malformed, or missing. Check the
Authorizationheader format. Check if the auth server/service is reachable. - If 403: The token is valid but lacks required permissions. Check role assignments, resource ownership, and permission policies.
API Response Anti-Patterns
200 with error in body — Don't do this:
HTTP/1.1 200 OK
{ "success": false, "error": "User not found" }
This breaks every HTTP client, monitoring tool, and retry mechanism that relies on status codes. Use 404.
Using 200 for everything — Some APIs return 200 for every response and encode the real status in the body. This makes the API unusable with standard HTTP tooling, breaks caching, breaks CDNs, and makes monitoring impossible.
500 for validation errors — If the client sent bad data, that's a 400 or 422, not a 500. Returning 500 triggers server error alerts, masks real server problems, and tells clients they should retry (they shouldn't — the same bad data will fail again).
Status Codes in REST API Design
A well-designed REST API uses status codes consistently:
| Operation | Success | Common Errors |
|---|---|---|
GET /resources | 200 (list) | 401, 403 |
GET /resources/:id | 200 (item) | 401, 403, 404 |
POST /resources | 201 (created) | 400, 401, 403, 409, 422 |
PUT /resources/:id | 200 (updated) | 400, 401, 403, 404, 409, 422 |
PATCH /resources/:id | 200 (partial update) | 400, 401, 403, 404, 422 |
DELETE /resources/:id | 204 (no content) | 401, 403, 404 |
Error Response Format
Pick a consistent error format and use it everywhere:
{
"error": {
"code": "validation_error",
"message": "Request validation failed",
"details": [
{ "field": "email", "message": "must be a valid email address" },
{ "field": "age", "message": "must be a positive integer" }
]
}
}
Status Codes and Caching
Status codes directly control HTTP caching behavior:
- 200 — Cacheable by default (with appropriate
Cache-Controlheaders) - 301 — Cached permanently by browsers (be careful — hard to undo)
- 302/307 — Not cached by default
- 304 — Tells the client to use its cached version
- 404 — Can be cached (negative caching prevents repeated lookups for missing resources)
- 500/502/503 — Should not be cached (they're temporary failures)
Misconfigured caching with the wrong status codes causes some of the hardest bugs to debug. A 301 that should have been a 302 will be cached by browsers indefinitely, and the only fix is users clearing their browser cache — individually.
Quick Reference
200 OK Everything worked
201 Created New resource created (POST)
204 No Content Success, empty body (DELETE)
301 Moved Permanently Permanent URL change (SEO redirect)
302 Found Temporary redirect
304 Not Modified Use cached version
400 Bad Request Malformed request
401 Unauthorized Need to authenticate
403 Forbidden Authenticated but not allowed
404 Not Found Resource doesn't exist
405 Method Not Allowed Wrong HTTP method
409 Conflict State conflict (duplicate, stale update)
422 Unprocessable Entity Valid syntax, invalid semantics
429 Too Many Requests Rate limited
500 Internal Server Error Server crashed
502 Bad Gateway Upstream server sent bad response
503 Service Unavailable Server overloaded or in maintenance
504 Gateway Timeout Upstream server didn't respond in time
HTTP status codes are a shared vocabulary. When your API speaks it correctly, every client library, monitoring tool, CDN, load balancer, and browser in the ecosystem works better. When it doesn't, you spend your time building workarounds for problems that HTTP already solved.
