HTTP Status Codes: Complete Reference Guide (200, 404, 500 & More)
Here is a scenario that plays out in production every week: an API returns 200 OK with a body of { "error": "User not found" }. A mobile client sees the 200, skips error handling, and silently renders a broken screen. The fix is not a clever workaround — it is returning 404 in the first place.
HTTP status codes are a contract between server and client, defined in RFC 9110 (HTTP Semantics, published June 2022 by the IETF, superseding RFC 7231). There are 60+ registered codes across five classes. Using the wrong one breaks client logic, corrupts SEO signals, and makes your API hard to integrate. This guide covers every class with precise usage rules, the code pairs developers most often confuse, and a complete lookup table for quick reference.
Key Takeaways
- ›RFC 9110 (IETF, 2022) is the authoritative specification for HTTP status codes — it supersedes RFC 7231 and all earlier HTTP/1.1 specs.
- ›The first digit tells you the class: 1xx informational, 2xx success, 3xx redirect, 4xx client error, 5xx server error. An unknown code in a class can be treated as its x00 baseline.
- ›401 means unauthenticated (not who you are), 403 means unauthorized (you are known but forbidden). The names are misleading.
- ›301 passes SEO link equity; 302 does not. Using the wrong redirect type is an unrecoverable SEO mistake until you correct it.
- ›For API design: GET returns 200, POST returns 201, DELETE returns 204. Deviating from this without good reason creates integration friction.
Complete HTTP Status Code Reference
The table below covers every status code you will commonly encounter in production. For a quick interactive lookup, use the HTTP Status Codes tool — enter any code and get its description, RFC reference, and common causes.
1xx — Informational
Provisional responses indicating the request was received and processing continues. Most HTTP clients handle these transparently — you will rarely write code that explicitly reads a 1xx response.
| Code | Name | Practical Usage |
|---|---|---|
| 100 | Continue | Server received headers; client should send body. Used with Expect: 100-continue for large uploads |
| 101 | Switching Protocols | Protocol upgrade — the mechanism behind every WebSocket handshake |
| 102 | Processing (WebDAV) | Server accepted a long-running WebDAV request; prevents client timeout |
| 103 | Early Hints | Send Link preload headers before the final response — improves LCP by parallelizing resource fetches |
103 Early Hints is worth knowing in 2026. Supported by Chrome, Firefox, and Safari, it lets your server send Link: </styles.css>; rel=preload headers before it finishes computing the response. According to Google's Web Platform team data published on web.dev, Early Hints can reduce Largest Contentful Paint by 20–30% on server-rendered pages where the critical CSS path is predictable.
2xx — Success
The server understood and fulfilled the request. Each 2xx code signals a subtly different kind of success — using the right one makes your API self-documenting.
| Code | Name | HTTP Methods | Response Body? |
|---|---|---|---|
| 200 | OK | GET, PUT, PATCH, POST (actions) | Yes |
| 201 | Created | POST | Yes — include Location header |
| 202 | Accepted | POST, PUT | Yes — return job ID/status URL |
| 204 | No Content | DELETE, PUT, PATCH | No |
| 206 | Partial Content | GET | Yes — partial bytes per Range header |
| 207 | Multi-Status (WebDAV) | POST, DELETE | Yes — XML body with per-item statuses |
// 201 Created — always include Location pointing to the new resource
app.post('/api/users', async (req, res) => {
const user = await db.createUser(req.body);
res
.status(201)
.header('Location', `/api/users/${user.id}`)
.json(user);
});
// 202 Accepted — async job queued; client polls the job URL
app.post('/api/exports', async (req, res) => {
const job = await queue.enqueue('export', req.body);
res.status(202).json({
jobId: job.id,
statusUrl: `/api/jobs/${job.id}`,
estimatedCompletionMs: 5000,
});
});
// 204 No Content — successful DELETE, no body
app.delete('/api/users/:id', async (req, res) => {
await db.deleteUser(req.params.id);
res.status(204).send();
});3xx — Redirection
The client must take additional action — usually following a Location header — to complete the request. Choosing the wrong redirect code is a silent SEO mistake that can persist for months.
| Code | Name | Permanent? | Preserves Method? | SEO Equity |
|---|---|---|---|---|
| 301 | Moved Permanently | Yes | No (may change to GET) | Passes |
| 302 | Found | No | No (may change to GET) | Does not pass |
| 303 | See Other | No | Always GET | Does not pass |
| 304 | Not Modified | N/A | N/A | N/A — cache hit |
| 307 | Temporary Redirect | No | Yes | Does not pass |
| 308 | Permanent Redirect | Yes | Yes | Passes |
The critical decision: use 308 instead of 301 when redirecting POST/PUT/PATCH endpoints. RFC 9110 notes that 301 and 302 are historically ambiguous — browsers started converting POST redirects to GET, so 301 on a form submission effectively silently drops your POST body. 307 and 308 were introduced to fix this: they guarantee the HTTP method is preserved. For API redirects, always use 307 (temporary) or 308 (permanent), never 301/302.
304 Not Modified is often overlooked but critical for performance. When a client sends an If-None-Match (ETag) or If-Modified-Since header and the resource has not changed, the server returns 304 with no body. The client uses its cached copy. According to MDN Web Docs, proper ETag validation is one of the most effective HTTP caching mechanisms for reducing bandwidth — a 304 response can be as small as a few hundred bytes vs. kilobytes for the full resource.
4xx — Client Errors
Something is wrong with the request. The server understood it but refuses to process it as-is. Every 4xx response should include a human-readable error message in the body — a bare 404 with no body is unhelpful to both developers and users.
| Code | Name | Common Cause | Retry? |
|---|---|---|---|
| 400 | Bad Request | Malformed JSON, missing required field, invalid param type | No — fix request |
| 401 | Unauthorized | Missing token, expired JWT, invalid API key | Retry after auth |
| 403 | Forbidden | Authenticated but insufficient role/scope | No — need permission |
| 404 | Not Found | Resource does not exist, wrong URL | No |
| 405 | Method Not Allowed | POST to a GET-only endpoint | No — fix method |
| 408 | Request Timeout | Client took too long to send the request | Yes |
| 409 | Conflict | Duplicate resource, optimistic locking failure | Yes — resolve conflict |
| 410 | Gone | Resource permanently deleted — will never return | No |
| 413 | Content Too Large | Request body exceeds server limit (Nginx default: 1MB) | No — reduce payload |
| 415 | Unsupported Media Type | Wrong Content-Type header (e.g., sending XML to a JSON-only API) | No — fix Content-Type |
| 422 | Unprocessable Entity | Valid syntax, failed business logic validation | No — fix values |
| 425 | Too Early | TLS Early Data replay risk; server refuses 0-RTT request | Yes — without Early Data |
| 429 | Too Many Requests | Rate limit exceeded | Yes — after Retry-After |
| 451 | Unavailable For Legal Reasons | Content blocked by legal order (GDPR, DMCA, court injunction) | No |
The Codes Developers Most Often Confuse
401 vs 403: Authentication vs Authorization
The RFC 9110 spec is explicit but the names are backwards from what most people expect. 401 Unauthorized semantically means "unauthenticated" — the server does not know who you are. It requires a WWW-Authenticate header that tells the client how to authenticate (e.g., Bearer, Basic). 403 Forbidden means the server knows exactly who you are but you do not have permission for this resource.
A security note: returning 403 when a resource does not exist for the requesting user reveals information. If user A should not know whether user B's private resource exists, return 404 instead of 403. This prevents enumeration attacks. Per OWASP API Security Top 10 (2023 edition), broken object-level authorization (BOLA) is the #1 API vulnerability — 404-masking is one mitigation.
// 401 — No token or invalid token (server doesn't know who you are)
if (!token || !verifyToken(token)) {
return res
.status(401)
.header('WWW-Authenticate', 'Bearer realm="api"')
.json({ error: 'Authentication required' });
}
// 403 — Valid token but wrong role
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
// Security: return 404 instead of 403 when existence itself is sensitive
const doc = await db.findDoc(req.params.id);
if (!doc || doc.ownerId !== req.user.id) {
// Don't reveal the doc exists — return 404 either way
return res.status(404).json({ error: 'Document not found' });
}400 vs 422: Parse Error vs Validation Error
400 Bad Request is for requests the server cannot parse at all — malformed JSON, invalid URL encoding, missing required headers, or a query parameter that cannot be cast to the expected type. 422 Unprocessable Entity (introduced for WebDAV in RFC 4918, generalized in RFC 9110) is for requests the server can parse but that fail semantic validation — valid JSON with a field value outside the accepted range, a duplicate email on registration, or a business rule violation.
In practice: if your JSON parser throws a SyntaxError, return 400. If your validator (Zod, Joi, express-validator) rejects the parsed data, return 422. The distinction matters for API clients — a 400 means fix the structure, a 422 means fix the values.
502 vs 503 vs 504: Reverse Proxy Errors
All three come from your reverse proxy (Nginx, Cloudflare, AWS ALB, Traefik) when it cannot get a valid response from your application server. The distinction tells you exactly where to look:
- 502 Bad Gateway — Proxy reached the upstream server but got an invalid or empty response. Your app process is likely crashed. Check
systemctl statusor your container runtime. - 503 Service Unavailable — Proxy cannot reach the upstream at all, or the upstream is deliberately rejecting connections (overloaded, maintenance mode, health check failing). Check your load balancer's health check status.
- 504 Gateway Timeout — Proxy reached the upstream but it did not respond within the configured timeout. Check for slow database queries, network latency, or CPU saturation. Per Nginx documentation, the default
proxy_read_timeoutis 60 seconds.
5xx — Server Errors
Server errors mean the request was valid but the server failed to fulfill it. Every 5xx in production is a bug, misconfiguration, or capacity issue. These should all be monitored and alerting on sustained 5xx rates is table stakes for any production service.
| Code | Name | Common Cause | Retry? |
|---|---|---|---|
| 500 | Internal Server Error | Unhandled exception, null pointer, logic error | Maybe |
| 501 | Not Implemented | HTTP method not supported by this server | No |
| 502 | Bad Gateway | Upstream returned invalid response (app crashed) | Yes — after delay |
| 503 | Service Unavailable | Overloaded, maintenance, health check failing | Yes — after Retry-After |
| 504 | Gateway Timeout | Upstream too slow; slow DB query, CPU saturation | Yes — idempotent requests only |
| 507 | Insufficient Storage (WebDAV) | Disk full; cannot complete write operation | No — free space first |
// Global error handler — catches any unhandled exceptions
// Never expose stack traces to clients in production
app.use((err, req, res, next) => {
// Log the full error server-side
logger.error({ err, reqId: req.id }, 'Unhandled error');
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message,
});
});
// 503 with Retry-After for graceful maintenance mode
app.use((req, res, next) => {
if (maintenanceMode.isEnabled()) {
return res
.status(503)
.header('Retry-After', String(maintenanceMode.expectedDurationSec()))
.json({ error: 'Scheduled maintenance in progress' });
}
next();
});Building Resilient Retry Logic Around Status Codes
A well-designed HTTP client should not retry blindly. Status codes tell you exactly which errors are recoverable. According to the Postman 2025 State of the API Report, which surveyed over 5,700 developers, unreliable APIs are the #1 developer pain point — and a major source of that unreliability is clients that crash on unexpected status codes instead of implementing graceful retry behavior.
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, options);
// Success — done
if (res.ok) return res;
// 4xx client errors — do NOT retry (except 408, 425, 429)
if (res.status >= 400 && res.status < 500) {
if (res.status === 429) {
const retryAfter = res.headers.get('Retry-After');
const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : 1000;
await sleep(waitMs);
continue;
}
// 401 — try refreshing the token once
if (res.status === 401 && attempt === 0) {
await refreshAccessToken();
options.headers.Authorization = `Bearer ${getToken()}`;
continue;
}
throw new ClientError(res.status, await res.json());
}
// 5xx server errors — retry with exponential backoff
if (attempt < maxRetries) {
await sleep(Math.pow(2, attempt) * 200); // 200ms, 400ms, 800ms
continue;
}
throw new ServerError(res.status, 'Max retries exceeded');
}
}One important nuance: only retry idempotent requests (GET, PUT, DELETE, HEAD) on 5xx errors. Retrying a POST on a 502 can create duplicate resources if the original request actually succeeded server-side but the response was lost in transit. Use idempotency keys (a unique request ID sent in a header) if you need safe POST retries.
Status Codes and HTTP Caching
Not all status codes are cacheable by default. Per RFC 9110, the following codes are cacheable without explicit Cache-Control headers: 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501. Note that 404 and 410 are cacheable — a CDN will cache your 404 responses unless you explicitly set Cache-Control: no-store or a short max-age. This causes the "cached 404" problem after you add a new route.
# Nginx — prevent CDN from caching 404 errors
location / {
try_files $uri $uri/ =404;
# Add this to prevent caching 404 responses
error_page 404 /404.html;
}
location = /404.html {
add_header Cache-Control "no-store, no-cache";
internal;
}
# For APIs, set short TTLs on error responses
location /api/ {
proxy_pass http://app;
# Cache 5xx errors for 5 seconds to absorb traffic spikes
# But don't cache 4xx — they depend on request content
proxy_cache_valid 200 201 204 10m;
proxy_cache_valid 404 410 1m;
proxy_cache_valid 500 502 503 504 5s;
}HTTP Status Codes and SEO: What Google Actually Does
Googlebot treats status codes as signals for crawl budget and indexing. According to Google Search Central documentation (2025), here is how Googlebot responds to each class:
- 200 — Page is crawled and eligible for indexing.
- 301/308 — Link equity is passed to the destination URL. Google follows up to 10 redirect hops before giving up.
- 302/307 — Temporary redirect; Google indexes the source URL, not the destination. No link equity passed.
- 404 — Google removes the URL from its index after seeing consistent 404s over several crawl cycles (usually days to weeks). The URL stays in Search Console for 90 days after removal.
- 410 — Google deindexes the URL faster than 404, typically within one or two crawl cycles. Use 410 for content you want removed from search results quickly.
- 500/503 — Google temporarily reduces crawl rate. If 500s persist for more than a few days, Google may begin removing the affected URLs from its index.
- noindex in 200 — Overrides everything; the URL will not be indexed regardless of other signals.
A common site migration mistake: returning 200 on a "soft 404" page (a page that says "content not found" but returns HTTP 200). Google Search Console flags these as "Soft 404" errors, and the pages consume crawl budget without contributing to rankings. Always return a real 404 or 410 for genuinely missing content.
Implementing 429 Rate Limiting Correctly
RFC 6585 defines 429 Too Many Requests and specifies that it should include a Retry-After header. Beyond that minimum, well-designed APIs send proactive rate limit headers on every response so clients never hit the limit by surprise. The de-facto standard, used by GitHub, Stripe, Cloudflare, and most major APIs, is:
// Rate limit headers on every response (not just 429)
X-RateLimit-Limit: 1000 // Total requests allowed in window
X-RateLimit-Remaining: 847 // Requests remaining in current window
X-RateLimit-Reset: 1742659200 // Unix timestamp when window resets
// 429 response
HTTP/1.1 429 Too Many Requests
Retry-After: 47
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1742659200
Content-Type: application/json
{
"error": "rate_limit_exceeded",
"message": "API rate limit exceeded. Retry in 47 seconds.",
"retryAfter": 47
}For API authentication endpoints specifically, rate limiting is a security control, not just a capacity measure. Without it, a credential-stuffing attack can test thousands of passwords per second. The OWASP API Security Top 10 (2023) lists "Unrestricted Resource Consumption" as a top vulnerability — rate limiting on auth endpoints is mandatory, not optional.
Standardizing Error Response Bodies
RFC 9457 (Problem Details for HTTP APIs, July 2023) defines a standard JSON format for HTTP error responses. It is not widely enforced but widely recommended. The format uses Content-Type: application/problem+json:
// RFC 9457 Problem Details format
{
"type": "https://api.example.com/errors/validation-failed",
"title": "Validation Failed",
"status": 422,
"detail": "The request body contains invalid fields.",
"instance": "/api/users",
"errors": [
{ "field": "email", "message": "Must be a valid email address" },
{ "field": "age", "message": "Must be between 18 and 120" }
]
}
// Simpler format used by most production APIs
{
"error": "validation_failed",
"message": "Request validation failed",
"details": [
{ "field": "email", "code": "INVALID_FORMAT", "message": "Must be a valid email address" }
],
"requestId": "req_01HV7X2P3K4Y5Z6W7Q8R9S0T"
}Always include a machine-readable error code (not just a human message), a unique request ID for support debugging, and a details array for validation errors. Use our JSON Formatter to validate that your error response structures are well-formed before shipping them.
API Design Decision Cheat Sheet
The most common question when building REST APIs: "which status code should I return for X?" This table covers the scenarios that come up in every project.
| Scenario | Correct Code | Common Mistake |
|---|---|---|
| GET returns data | 200 | — |
| GET returns empty list | 200 [] | 404 (wrong — empty is not missing) |
| POST creates resource | 201 + Location | 200 (missing Location header) |
| DELETE resource | 204 | 200 (not wrong, but 204 is cleaner) |
| Delete already-deleted resource | 204 or 404 | Context-dependent: idempotent DELETE returns 204 |
| Async job queued | 202 + status URL | 200 (implies synchronous completion) |
| JSON body malformed | 400 | 500 (if parser throws unhandled) |
| Validation fails (valid JSON) | 422 | 400 (reserved for parse errors) |
| Duplicate record on POST | 409 | 400 (too generic) |
| Missing auth token | 401 + WWW-Authenticate | 403 (conflates auth and authz) |
| Wrong role / insufficient scope | 403 | 401 (confuses authentication with authorization) |
| Permanent URL move | 301 (GET) or 308 (POST) | 302 (loses SEO equity) |
Debugging HTTP Status Codes in Development
When debugging HTTP responses, these tools in your workflow save the most time:
- cURL with -I or -v —
curl -I https://example.comfor headers only;-vfor full request/response trace including TLS handshake - Browser DevTools Network tab — filter by status code using the filter bar;
status-code:5xxsyntax works in Chrome - httpstat.us — test endpoint that returns any status code you specify for testing client error handling
When building APIs that return JSON responses, validating your error body structure is as important as setting the correct status code. A 422 with a poorly structured body is harder to integrate than a well-structured 400.
For encoding issues in URL construction — which frequently cause 400 errors — use the URL Encoder/Decoder to verify that special characters in query parameters and path segments are correctly percent-encoded per RFC 3986.
Frequently Asked Questions
What is the difference between 401 and 403?
401 Unauthorized means the server does not know who you are — authentication is missing or invalid. RFC 9110 requires a WWW-Authenticate header telling the client how to authenticate. 403 Forbidden means the server knows exactly who you are but you lack permission for the resource. The naming is misleading: 401 is really "unauthenticated" and 403 is truly "unauthorized."
When should I use 200 vs 201 vs 204?
Use 200 OK for successful GET, PUT, and PATCH requests that return data. Use 201 Created after a POST that creates a new resource — always include a Location header pointing to the new resource URL. Use 204 No Content for successful DELETE operations or PUT/PATCH where you deliberately return no body. Using 200 for everything is one of the most common REST API mistakes.
What is the difference between 301 and 302 redirects?
301 Moved Permanently tells search engines and browsers this URL has changed forever — link equity (PageRank) transfers to the new URL and the redirect is cached indefinitely. 302 Found is a temporary redirect that does not transfer SEO value and should not be cached. For permanent URL changes, always use 301 or 308. For A/B testing or maintenance pages, use 302 or 307.
What causes a 502 Bad Gateway error?
A 502 Bad Gateway means your reverse proxy received an invalid or empty response from the upstream application server. Common causes: the application process crashed, memory was exhausted, or a firewall blocked the connection. Check application logs first, then proxy error logs. Distinct from 503 (app is alive but refusing connections) and 504 (app responded but too slowly).
Should I use 404 or 410 for deleted content?
Use 410 Gone when you know a resource has been permanently deleted and will never return. Per RFC 9110, 410 tells search engines to remove the URL from their index immediately, whereas 404 suggests the resource might return. For content you cannot track, 404 is fine. If you maintain a deletion log, return 410 — Google respects 410 faster than 404 for deindexing.
What does 422 Unprocessable Entity mean vs 400 Bad Request?
400 Bad Request means the request is syntactically malformed — the server cannot parse it at all. 422 Unprocessable Entity means the request is syntactically valid (parseable) but semantically invalid — a required field is missing or a value fails business logic validation. Use 400 for parse errors and 422 for validation errors. The distinction helps API clients know whether to fix the structure or fix the values.
What HTTP status code should an API return for rate limiting?
Return 429 Too Many Requests with a Retry-After header specifying when the client can retry. Also include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers on all responses so clients can track their quota proactively and avoid hitting the limit.
Is 204 No Content the right code after a DELETE?
Yes — 204 No Content is the standard for a successful DELETE that returns no body. Some APIs return 200 with the deleted resource in the body, which is also acceptable per RFC 9110. What you should never do is return 404 after successfully deleting a resource — the deletion succeeded, so a 2xx code is correct. An idempotent DELETE of an already-deleted resource can legitimately return either 204 or 404 depending on your API contract.
Look Up Any HTTP Status Code Instantly
Enter any three-digit status code and get its RFC reference, description, common causes, and usage guidance — no scrolling through tables.
Open HTTP Status Codes Tool