~/guides/-blog-http-status-codes-guide-
guides · HTTP

HTTP Status Codes: A Developer's Complete Reference

What every HTTP status code actually means, when to use each one in your API, and the common mistakes that send clients the wrong code.

last updated · June 13, 2026by @vultio

The five classes: why the first digit matters most

HTTP status codes are three-digit numbers grouped into five classes by their leading digit. Before memorising individual codes, understand what each class signals — because clients, proxies, and CDNs all make decisions based on the class, not the exact code. An unknown 4xx code a client has never seen before will still be treated as a client error. An unknown 5xx will still trigger retry logic in a well-behaved HTTP client.

1xx — Informational

The request was received and processing is continuing. Rarely seen in REST APIs. The most practical example is 101 Switching Protocols, used when upgrading an HTTP connection to WebSocket.

2xx — Success

The request was received, understood, and accepted. This is the class you return when everything worked. The specific 2xx code carries meaning about what happened — created, deleted, or just succeeded.

3xx — Redirection

Further action is required to complete the request. Usually a Location header tells the client where to go. Whether the redirect is permanent or temporary, and whether the HTTP method is preserved, varies by code.

4xx — Client Error

The request contains bad syntax or cannot be fulfilled as sent. The problem is on the caller's side. Retrying the identical request without modification will not help.

5xx — Server Error

The server failed to fulfil an apparently valid request. The problem is on the server's side. The client did nothing wrong, and retrying (with appropriate backoff) may succeed.

The essential 2xx codes

Most APIs only need three 2xx codes for the majority of their endpoints. Choosing the right one adds precision that clients can act on automatically.

200 OK is the general success code. Use it for GET requests that return a resource, PUT/PATCH requests that update and return the updated representation, and POST requests that perform an action rather than create a resource. It is the right default when no more specific 2xx applies.

201 Created is for POST requests that result in a new resource being created on the server. Include a Location header pointing to the newly created resource. Clients and frameworks use this header to navigate to the new object without a separate lookup. If you return 200 from a resource-creation endpoint, you are discarding information the caller needs.

204 No Content means success with no response body. Use it for DELETE requests, and for PUT/PATCH requests where you intentionally do not return the updated resource. Do not return 204 from a GET — callers expect a body. Do not return 200 with an empty body when you mean 204 — the distinction matters to clients that parse the response body based on status.

# POST /users — creates a resource
HTTP/1.1 201 Created
Location: /users/42
Content-Type: application/json

{ "id": 42, "email": "user@example.com" }

# DELETE /users/42 — no body needed
HTTP/1.1 204 No Content

Redirect codes: 301 vs 302 vs 307 vs 308

The four redirect codes split along two axes: permanent versus temporary, and whether the HTTP method is preserved. Getting this wrong has consequences both for API consumers and for SEO when HTML pages are involved.

301 Moved Permanently

The resource has moved to the Location URL permanently. Browsers and clients cache this indefinitely. Search engines transfer link equity to the new URL. The practical problem: browsers will change POST to GET on a 301 redirect (legacy browser behaviour). Use 308 if you need to permanently redirect a non-GET endpoint.

302 Found

Temporary redirect. Clients should not cache it. The resource is still at the original URL conceptually, just temporarily served from elsewhere. Like 301, browsers typically change POST to GET on a 302 redirect. Use 307 for temporary redirects that must preserve the method.

307 Temporary Redirect

Temporary redirect that explicitly preserves the HTTP method. A POST to the original URL will POST to the new URL. Use this when you temporarily move an endpoint and the caller's method must not be changed.

308 Permanent Redirect

Permanent redirect that explicitly preserves the HTTP method. The permanent equivalent of 307. Use it for permanent endpoint renames where callers must continue using the same HTTP verb. Prefer 308 over 301 for API endpoints that accept POST/PUT/DELETE.

For SEO: use 301 (or 308 for non-GET pages, though 301 is most widely understood by crawlers) when you permanently move a page. Using 302 when you mean permanent loses the ranking signals of the original URL — search engines do not consolidate equity through temporary redirects. For API versioning, 308 is the cleaner choice for permanent moves because it prevents accidental GET downgrades.

The critical 4xx codes every API needs

Client error codes are where most APIs diverge from the spec — either returning 400 for everything, or inventing distinctions the spec already covers. Here are the codes that have clear, distinct meanings your API should honour.

400 Bad Request

The request syntax is malformed, the parameters are invalid, or the request is otherwise nonsensical and cannot be processed. Use this as a general fallback for invalid input when a more specific code does not apply.

401 Unauthorized

Despite the name, this means unauthenticated. The client has not provided credentials, or the credentials it provided are invalid, expired, or unrecognisable. The server is saying "I don't know who you are." Include a WWW-Authenticate header indicating how to authenticate.

403 Forbidden

The client is authenticated — the server knows who they are — but they are not allowed to access this resource or perform this action. The credentials are valid; the permission is not. This is not an authentication failure.

404 Not Found

The requested resource does not exist at this URL. Also intentionally returned in place of 403 when you want to conceal whether a resource exists from unauthorised callers.

409 Conflict

The request conflicts with the current state of the server. Classic use cases: creating a user with an email address that already exists, or trying to publish a resource that is in a state that does not allow publishing.

422 Unprocessable Entity

The request is syntactically valid and the server understood it, but the semantic content fails validation. Preferred by most REST API conventions for field-level validation errors over 400, because it signals "I understood what you sent, but the values are wrong."

429 Too Many Requests

The caller has exceeded a rate limit. Include a Retry-After header with the number of seconds to wait, or an X-RateLimit-Reset timestamp. Clients should back off automatically on 429; make it easy for them to do so.

The 5xx codes: server-side failures

5xx codes tell the client the server failed. They imply the request might succeed if retried later. Well-designed clients implement exponential backoff on 5xx responses.

500 Internal Server Error

An unexpected condition prevented the server from fulfilling the request. The general catch-all for unhandled exceptions. Log the full error server-side and return a safe, generic message to the client — never expose stack traces or internal details in the response body.

502 Bad Gateway

The server was acting as a gateway or proxy and received an invalid response from an upstream server. Common when a load balancer or reverse proxy cannot reach the application server, or the application returned a malformed response.

503 Service Unavailable

The server is temporarily unable to handle the request — typically because it is overloaded or down for maintenance. Always include a Retry-After header. Without it, clients have no signal for when to retry and may hammer the server immediately.

504 Gateway Timeout

The gateway or proxy did not receive a timely response from an upstream server. Distinct from 503 — the upstream was reachable but too slow. Common in microservice architectures when a dependency times out under load.

# 503 with Retry-After — tells the client exactly when to try again
HTTP/1.1 503 Service Unavailable
Retry-After: 30
Content-Type: application/json

{
  "error": "service_unavailable",
  "message": "The service is temporarily unavailable. Please retry in 30 seconds.",
  "request_id": "req_9kLmNpQ2"
}

401 vs 403: the confusion most APIs get wrong

This is the most common status code mistake in production APIs. The names are misleading: 401 is called "Unauthorized" but it means unauthenticated. 403 is called "Forbidden" and it actually means unauthorised. The HTTP spec uses "authorized" in the sense of "having authority to act" — which is the result of authentication, not authorisation in the access-control sense. The naming stuck, and the confusion has persisted ever since.

Return 401 when: The request lacks credentials entirely, the token is absent, the API key is missing, the session has expired, or the credentials presented cannot be verified. The server is asking "who are you?" A 401 should always come with a WWW-Authenticate header telling the client how to authenticate.

Return 403 when: The client is authenticated — the server knows exactly who they are — but their identity does not grant access to this resource or action. The user is logged in but lacks the required role. The API key is valid but scoped to read-only. The resource belongs to a different tenant. The answer is "I know who you are, and the answer is no."

// Middleware decision tree
function authMiddleware(req, res, next) {
  const token = req.headers.authorization;

  if (!token) {
    // No credentials at all — 401
    return res.status(401).set('WWW-Authenticate', 'Bearer').json({
      error: 'authentication_required',
      message: 'No credentials provided.'
    });
  }

  const user = verifyToken(token);
  if (!user) {
    // Credentials present but invalid/expired — 401
    return res.status(401).set('WWW-Authenticate', 'Bearer error="invalid_token"').json({
      error: 'invalid_token',
      message: 'The token is invalid or has expired.'
    });
  }

  if (!user.hasPermission(req.route)) {
    // Valid user, wrong permissions — 403
    return res.status(403).json({
      error: 'forbidden',
      message: 'You do not have permission to perform this action.'
    });
  }

  req.user = user;
  next();
}

404 vs 403: when hiding resource existence is correct

Returning 403 when an unauthorised user requests a resource reveals that the resource exists. Sometimes that is acceptable — returning 403 for /admin/dashboard tells a regular user they cannot access that page, which is fine. But for sensitive or private resources, confirming existence to an unauthenticated caller can be a security leak.

Consider GET /users/jane.doe/private-documents. If an attacker receives a 403, they know jane.doe exists and has private documents. If they receive a 404, they learn nothing about whether that user or those documents exist. For APIs that handle sensitive user data, returning 404 instead of 403 when the caller is unauthorised is a deliberate privacy decision, not a mistake.

The trade-off: 404 makes debugging harder for legitimate callers who have misconfigured their permissions. Apply 404-for-unauthorised selectively — for resources where confirming existence is a meaningful information leak — and document it in your API so developers do not waste time thinking the URL is wrong.

Validation errors: 400 or 422?

Both 400 and 422 are used for validation failures in practice, and the distinction is subtle. Strictly reading the spec: 400 is for requests that are syntactically malformed — a JSON body that is not valid JSON, a missing required header, a query parameter with the wrong type. 422 is for requests that are syntactically valid but semantically invalid — the JSON parses fine, the fields are present, but the values fail business rules.

In practice, most modern REST APIs and frameworks use 422 for field-level validation errors and 400 for lower-level problems like malformed JSON or missing Content-Type. This convention is followed by Rails, FastAPI, and many OpenAPI-based frameworks. If you adopt it, 422 signals "the schema is right, the values are wrong" and clients know to inspect the error body for field-specific messages.

# 400 — the request body is not valid JSON
HTTP/1.1 400 Bad Request
{ "error": "invalid_json", "message": "Request body could not be parsed as JSON." }

# 422 — valid JSON, but field values fail validation
HTTP/1.1 422 Unprocessable Entity
{
  "error": "validation_failed",
  "message": "One or more fields failed validation.",
  "fields": {
    "email": "Must be a valid email address.",
    "age": "Must be 18 or older."
  }
}

API error response bodies: the status code is not enough

A status code tells clients the category of the problem. It does not tell them what to display to the user, how to log it, or how to distinguish two different errors that both return 400. A minimal useful error response body includes three things: a machine-readable error code, a human-readable message, and a request ID that can be correlated with server logs.

{
  "error": "rate_limit_exceeded",
  "message": "You have exceeded the rate limit of 100 requests per minute.",
  "request_id": "req_7xTqWkP1",
  "retry_after": 42
}

The error field is a stable, lowercase, underscore-separated identifier that clients can branch on in code — it never changes between API versions. The message is human-readable and may change. The request_id is generated per-request and logged server-side, so a developer can paste it into your support channel or search your log aggregator and find the exact failure.

Never expose internal details in error bodies: no stack traces, no database error messages, no file paths, no internal service names. These are 5xx scenarios where the caller needs to know something went wrong, not how your infrastructure is laid out.

Quick decision chart: which code should your endpoint return?

Work through this decision tree for any endpoint response scenario:

Did the request succeed?

Yes → 2xx. Did it create a resource? → 201 with Location header. Is there a response body? → 200. No body (DELETE, etc.)? → 204.

Is the client being redirected?

Permanent move? → 301 (or 308 if method must be preserved). Temporary move? → 302 (or 307 if method must be preserved).

Is the problem the client's fault?

No credentials at all or credentials invalid? → 401. Valid user but wrong permissions? → 403. Resource does not exist? → 404. Duplicate/conflict with server state? → 409. Validation failure on field values? → 422. Malformed request syntax? → 400. Rate limited? → 429 with Retry-After.

Is the problem the server's fault?

Unhandled exception? → 500. Upstream returned garbage? → 502. Server overloaded/maintenance? → 503 with Retry-After. Upstream timed out? → 504.

None of the above fit?

Use the most specific code in the right class. Unknown codes in a class are handled as the base code (e.g., an unrecognised 4xx is treated like 400). When genuinely uncertain between two 4xx codes, choose the more general one and document the reasoning.

Consistency matters as much as correctness. Clients that integrate with your API will build assumptions based on the codes you return. If you return 400 for validation errors in one endpoint and 422 in another, clients must handle both everywhere. Pick a convention for your API and apply it uniformly — a consistent 400-for-everything is less harmful than unpredictable mixing of semantically distinct codes.