Guides12 min read

HTTP Status Codes: The Complete Developer Guide

toolsto.dev
HTTP Status Codes: The Complete Developer Guide

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:

RangeClassMeaning
1xxInformationalRequest received, continuing process
2xxSuccessRequest received, understood, and accepted
3xxRedirectionFurther action needed to complete the request
4xxClient ErrorRequest contains bad syntax or can't be fulfilled
5xxServer ErrorServer 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:

  1. Check app server logs — is the process running?
  2. Check load balancer logs — what upstream error is it seeing?
  3. Check resource utilization — CPU, memory, disk, database connections
  4. 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 Authorization header 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:

OperationSuccessCommon Errors
GET /resources200 (list)401, 403
GET /resources/:id200 (item)401, 403, 404
POST /resources201 (created)400, 401, 403, 409, 422
PUT /resources/:id200 (updated)400, 401, 403, 404, 409, 422
PATCH /resources/:id200 (partial update)400, 401, 403, 404, 422
DELETE /resources/:id204 (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-Control headers)
  • 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.

Related Tools