HTTP Status Codes Guide: 1xx–5xx Classes, REST Conventions, and Retry Logic
HTTP status codes are the first words an API speaks. Before a client reads a single byte of the response body, the three-digit code tells it whether the request succeeded, where to redirect, or what went wrong. Misusing them breaks automatic retries, invalidates caches, confuses monitoring dashboards, and forces every consumer to parse a JSON body just to detect failure. Use the HTTP Status Reference alongside this guide to look up any code quickly.
The Five Classes
The first digit of every status code indicates the class. Understanding the class is more important than memorizing individual codes, because it tells you the semantics at a glance.
| Class | Range | Meaning | Body expected? |
|---|---|---|---|
| 1xx Informational | 100–199 | Request received, processing continues | No |
| 2xx Success | 200–299 | Request was received, understood, and accepted | Depends on code |
| 3xx Redirection | 300–399 | Further action needed to complete the request | Optional |
| 4xx Client Error | 400–499 | Request is malformed or not allowed | Usually yes (error detail) |
| 5xx Server Error | 500–599 | Server failed to fulfill a valid request | Usually yes (error detail) |
2xx Success in Detail
Most APIs overuse 200 OK and ignore the other 2xx codes. Each code carries precise semantics that clients can act on without parsing the body.
- 200 OK — Generic success. The response body contains the requested representation. Use for GET and POST operations that return data.
- 201 Created — A new resource was created. The
Locationheader should point to the new resource URI. Use for POST requests that create entities. - 202 Accepted — The request has been accepted for asynchronous processing but is not yet complete. Return a job ID or polling URL so the client can check back.
- 204 No Content — Success with no body. Clients must not try to parse a body. Use for DELETE and PUT operations that return nothing. Sending a 200 with an empty JSON object
{}instead of 204 is a common mistake. - 206 Partial Content — The server is delivering only part of a resource in response to a
Rangerequest header. Used by video streaming and resumable downloads. TheContent-Rangeheader specifies which bytes are included.
3xx Redirection Subtleties
Redirect codes differ in two ways: whether the redirect is permanent or temporary, and whether the HTTP method is preserved.
| Code | Permanent? | Method preserved? | Use when |
|---|---|---|---|
| 301 | Yes | No — may change to GET | Resource moved permanently; SEO juice passes to new URL |
| 302 | No | No — may change to GET | Temporary redirect; most browsers rewrite POST to GET |
| 307 | No | Yes — method must be preserved | Temporary redirect that must keep the original method (POST stays POST) |
| 308 | Yes | Yes — method must be preserved | Permanent redirect with method preservation |
| 304 | N/A | N/A | Cache validation: resource not modified since If-Modified-Since or If-None-Match; no body sent |
The practical rule: use 307 instead of 302 whenever you want to guarantee the method is preserved. Use 308 instead of 301 for permanent redirects on non-GET endpoints such as form submission handlers.
4xx Client Errors
4xx codes mean the client sent something the server cannot or will not process. These are not server bugs — they are signals back to the caller about what to fix.
- 400 Bad Request — The request is syntactically malformed: missing required fields, wrong JSON structure, unparseable date string. The client must fix the request before retrying.
- 401 Unauthorized — Authentication is required and has not been provided, or the credentials are invalid. Despite the name, this is about authentication, not authorization. Do not return 401 specifically for wrong passwords if the account exists — that leaks account existence. Return 401 generically for any invalid credential.
- 403 Forbidden — The client is authenticated but does not have permission. Use this when the identity is known but the action is not allowed.
- 404 Not Found — The resource does not exist at this URL. Safe to return when you intentionally want to hide a resource's existence (as opposed to 403 which confirms the resource exists).
- 410 Gone — The resource existed but has been permanently deleted and will not return. Unlike 404, it signals that the client should remove any saved reference. Useful for content that was deliberately retired.
- 409 Conflict — The request conflicts with current server state. Classic uses: duplicate unique key, optimistic concurrency conflict (version mismatch), trying to transition a resource to an invalid state.
- 422 Unprocessable Entity — The request is syntactically valid (well- formed JSON, correct content-type) but semantically invalid: validation failures, constraint violations, business rule rejections. This is where 422 beats 400 — use 400 for syntax problems, 422 for semantic/validation problems.
- 429 Too Many Requests — Rate limit exceeded. Should include a
Retry-Afterheader indicating when the client may try again.
400 vs 422 in practice
// 400 Bad Request — request body is not valid JSON
// POST /users with body: {name: "Alice" (missing closing brace)
// 422 Unprocessable Entity — valid JSON but fails validation
// POST /users with body:
{
"name": "",
"email": "not-an-email",
"age": -5
}
// Response body should list field-level errors:
{
"errors": [
{ "field": "name", "message": "must not be blank" },
{ "field": "email", "message": "invalid email format" },
{ "field": "age", "message": "must be a positive integer" }
]
}5xx Server Errors
5xx codes mean the server failed. The client sent a valid request and the server could not fulfill it. This is a server bug or operational issue, not a client mistake.
- 500 Internal Server Error — Generic catch-all for unhandled exceptions. Useful as a fallback but avoid using it for errors that have a more specific code.
- 501 Not Implemented — The server does not support the HTTP method used. Rare in APIs; most use 405 Method Not Allowed for disallowed methods on a specific route.
- 502 Bad Gateway — The server, acting as a proxy or gateway, received an invalid response from an upstream server. Common when a load balancer cannot reach an application instance.
- 503 Service Unavailable — The server is temporarily unable to handle the request: overloaded, in maintenance, or during deployment. Should include a
Retry-Afterheader. - 504 Gateway Timeout — The upstream server timed out. Common during slow database queries, external API calls, or cascading failures.
REST API Conventions
These are the standard mappings between CRUD operations and HTTP status codes that clients and frameworks expect.
| Operation | Method | Success code | Notes |
|---|---|---|---|
| List resources | GET /resources | 200 | Empty list is still 200, not 404 |
| Get single resource | GET /resources/:id | 200 | 404 if not found |
| Create resource | POST /resources | 201 | Include Location header pointing to the new resource |
| Full update | PUT /resources/:id | 200 or 204 | 200 if returning updated resource; 204 if not |
| Partial update | PATCH /resources/:id | 200 or 204 | Same as PUT |
| Delete resource | DELETE /resources/:id | 204 | No body; 404 if resource does not exist |
| Async action | POST /resources/:id/action | 202 | Return job ID or polling endpoint |
Retry-Safe Codes and the Retry-After Header
Not all error codes are safe to retry. Retrying a non-idempotent operation (like a payment POST) on the wrong code can create duplicates. These codes are the canonical retry candidates:
| Code | Retry? | Condition |
|---|---|---|
| 408 Request Timeout | Yes | The server timed out waiting for the request; safe to retry immediately |
| 429 Too Many Requests | Yes, after delay | Respect the Retry-After header; use exponential backoff if absent |
| 502 Bad Gateway | Yes | Upstream is temporarily unreachable; retry with backoff |
| 503 Service Unavailable | Yes, after delay | Check Retry-After; may indicate planned maintenance window |
| 504 Gateway Timeout | Yes | Upstream timed out; retry with backoff, but only if the operation is idempotent |
The Retry-After header value is either a number of seconds or an HTTP date:
# Seconds form
HTTP/1.1 429 Too Many Requests
Retry-After: 60
# Date form
HTTP/1.1 503 Service Unavailable
Retry-After: Mon, 21 Apr 2026 10:00:00 GMTIdempotency is the key safety property. GET, PUT, DELETE, and HEAD are idempotent by definition. POST is not unless the server implements an idempotency key (a client-provided unique request ID in a header like Idempotency-Key). Always make retries idempotent before enabling automatic retry logic on POST endpoints.
Common Anti-Patterns
- 200 with an error in the body. Returning
200 OKwith body{ "error": "User not found" }breaks every HTTP-aware tool: load balancers, APM monitors, retry middleware, and browser devtools all see a successful response. Use 4xx or 5xx codes so the transport layer can react correctly. - 500 for user input errors. If the user submits an invalid form, that is a client error, not a server error. Use 400 or 422. Returning 500 inflates your error rate dashboards and hides real server bugs.
- 401 for wrong password when account exists. Returning a specific message like “password incorrect” (as opposed to “invalid credentials”) confirms the account exists and enables user enumeration attacks. Return a generic 401 with a neutral message for any authentication failure.
- 404 vs 403 leaking resource existence. If you return 403 for a resource the requester should not know about, you have confirmed it exists. Return 404 when you want to hide existence entirely.
- Always returning 200 for redirects. Some legacy APIs return 200 with a redirect URL in the body instead of using 3xx. This forces clients to understand a custom protocol and prevents browsers and HTTP clients from following the redirect automatically.
Rare but Useful Codes
- 103 Early Hints — Sent before the final response to let clients start preloading resources (fonts, scripts, stylesheets) while the server is still preparing the response. Supported by Chrome and some CDNs. Useful on pages with large, predictable asset lists.
- 418 I'm a teapot — An April Fools joke from RFC 2324 (Hyper Text Coffee Pot Control Protocol). Kept in the registry as a novelty. Do not use in production.
- 451 Unavailable For Legal Reasons — The server is refusing to serve the resource for legal reasons (takedown order, GDPR compliance, geographic restriction). Named after the novel Fahrenheit 451. Preferred over 403 when the reason is specifically legal.
- 511 Network Authentication Required — The client must authenticate with the network (typically a captive portal) before access is granted. Used by hotel and airport WiFi systems to redirect users to a login page.
Testing Status Codes
Always write assertions against specific status codes, not just “success”. A test that only checks for a 2xx response will miss the difference between 200 and 204, and will not catch a regression where a 201 endpoint starts returning 200 without a Location header.
curl
# Print only the status code
curl -s -o /dev/null -w "%{http_code}" https://api.example.com/users/1
# Print status code and response body
curl -i https://api.example.com/users/1
# Fail (exit 1) on 4xx or 5xx — useful in CI scripts
curl -f https://api.example.com/healthHTTPie
# HTTPie displays the status line clearly
http GET https://api.example.com/users/1
# Check the exit code
http --check-status GET https://api.example.com/users/1 && echo "OK" || echo "FAILED"Writing assertions in tests
// Jest + supertest example
describe('POST /users', () => {
it('returns 201 with Location header on create', async () => {
const res = await request(app)
.post('/users')
.send({ name: 'Alice', email: 'alice@example.com' });
expect(res.status).toBe(201);
expect(res.headers['location']).toMatch(//users\/\d+/);
});
it('returns 422 with field errors on invalid input', async () => {
const res = await request(app)
.post('/users')
.send({ name: '', email: 'not-an-email' });
expect(res.status).toBe(422);
expect(res.body.errors).toHaveLength(2);
});
it('returns 429 when rate limit exceeded', async () => {
// hammer the endpoint beyond the limit
for (let i = 0; i < 100; i++) {
await request(app).post('/users').send({ name: 'x', email: 'x@x.com' });
}
const res = await request(app).post('/users').send({ name: 'y', email: 'y@y.com' });
expect(res.status).toBe(429);
expect(res.headers['retry-after']).toBeDefined();
});
});See API Debug Workflow for a practical guide to diagnosing failed requests, and Web Payload Workflow for inspecting request and response bodies in the browser. Use the HTTP Status Reference as a quick lookup when you encounter an unfamiliar code in a network trace.
HTTP status codes are a contract between client and server. Using the right code means clients can react correctly without reading the body, retries happen only when they are safe, caches are invalidated at the right moment, and monitoring systems count real errors instead of noise.