What are HTTP status codes?
HTTP status codes are three-digit numerical responses servers send to clients to communicate the outcome of an HTTP request. They are the universal language of the web: 200 OK means success, 404 Not Found means the resource doesn't exist, 500 Internal Server Error means something broke server-side. The current authoritative spec is RFC 9110 ("HTTP Semantics", 2022), which consolidates and supersedes RFC 2616, RFC 7230–7235, and several others.
Status codes split into five families based on the first digit:
| Class | Range | Meaning | Action by client |
|---|---|---|---|
| 1xx Informational | 100–199 | Provisional response — request received, processing | Wait for final response |
| 2xx Success | 200–299 | Request succeeded | Render / consume response |
| 3xx Redirection | 300–399 | Further action needed (usually a different URL) | Follow the redirect |
| 4xx Client error | 400–499 | Client sent something invalid or unauthorized | Fix the request, do not retry blindly |
| 5xx Server error | 500–599 | Server failed to fulfill a valid request | Retry with backoff; the server is at fault |
Why this matters: every layer of the modern web — browsers, CDNs, proxies, load balancers, monitoring systems, search engines — relies on status codes to make decisions. Send the wrong code and Google deindexes you, Cloudflare caches errors, retries hammer your servers, and users see broken UI.
The 12 most important HTTP status codes you'll actually use
| Code | Name | Use when |
|---|---|---|
| 200 | OK | Successful GET, successful PUT/PATCH that updated |
| 201 | Created | Successful POST that created a resource. Include Location: header. |
| 204 | No Content | Successful DELETE, or successful PUT with no body to return |
| 301 | Moved Permanently | URL changed forever. Search engines transfer SEO equity. Update bookmarks. |
| 302 / 307 | Found / Temporary Redirect | Temporary redirect. SEO equity stays on the original URL. |
| 304 | Not Modified | Conditional GET — client's cache is still valid. No body. |
| 400 | Bad Request | Malformed JSON, missing required field, type mismatch |
| 401 | Unauthorized | Missing or expired authentication. The user has NOT proven who they are. |
| 403 | Forbidden | Authenticated, but lacks permission for this resource |
| 404 | Not Found | Resource doesn't exist. Most-recognized error code in the world. |
| 409 | Conflict | State conflict — duplicate slug, version mismatch, simultaneous edit |
| 422 | Unprocessable Entity | Syntactically valid request but semantically invalid (validation failed) |
| 429 | Too Many Requests | Rate-limited. Include Retry-After: header. |
| 500 | Internal Server Error | Unhandled exception. Don't leak stack traces to users. |
| 502 | Bad Gateway | Upstream service (the API your server proxies to) failed |
| 503 | Service Unavailable | Server is overloaded or down for maintenance |
| 504 | Gateway Timeout | Upstream service didn't respond in time |
301 vs 302 vs 307 vs 308 — the redirect maze
Four codes cover redirects. Picking the wrong one breaks SEO, breaks form re-submission, or both.
| Code | Permanent? | Method preserved? | SEO transfers? | When to use |
|---|---|---|---|---|
| 301 Moved Permanently | Yes | Browsers may change POST → GET (legacy) | Yes — > 99% in 2026 | Permanent URL changes (site migrations, slug changes) |
| 302 Found | No | Browsers may change POST → GET (legacy) | No | Temporary redirect, A/B tests, geo-routing |
| 307 Temporary Redirect | No | Yes — POST stays POST | No | Modern temporary redirect when you need to preserve method/body |
| 308 Permanent Redirect | Yes | Yes — POST stays POST | Yes | Modern permanent redirect when method preservation matters |
The "POST → GET" gotcha (301/302 only)
Historically, browsers were inconsistent about whether to change a POST to a GET when following a 301 or 302. Most modern browsers preserve the method, but some legacy tools don't. Use 307/308 if method preservation matters. For plain URL redirects (which are the common case), 301 is still the right answer in 2026 — Google fully understands it transfers SEO equity, and ~all browsers handle it correctly.
401 vs 403 — Unauthorized vs Forbidden
Confused constantly. They mean different things:
401 Unauthorized — "Who are you?"
The client has NOT successfully authenticated. Either the credentials are missing entirely or invalid. The expected client behavior: prompt for login, retry with credentials.
- Missing
Authorizationheader - Expired JWT
- Invalid API key
- Wrong username/password
The response should include a WWW-Authenticate header indicating the auth scheme (Bearer, Basic, etc.).
403 Forbidden — "I know who you are, you can't do this"
The client IS authenticated but lacks permission. No amount of re-authentication will help — they need different permissions.
- Read-only API key trying to POST
- Free-tier user accessing premium endpoint
- Admin route accessed by non-admin user
- IP-based geo-blocking ("Sorry, not available in your region")
Mnemonic: 401 = "Authenticate yourself." 403 = "Authentication accepted; access still denied."
200 vs 201 vs 204 — the success codes
For REST APIs, picking the right success code communicates intent:
| Code | HTTP method | Body | Example |
|---|---|---|---|
| 200 OK | GET, PUT, PATCH | The current state of the resource | GET /users/42 → user JSON |
| 201 Created | POST | The new resource (or just Location:) | POST /users → 201 + Location: /users/43 |
| 202 Accepted | POST/PUT (async) | Job ID or status URL | POST /reports/generate → 202 (job queued) |
| 204 No Content | DELETE, PUT (no body) | Empty | DELETE /users/42 → 204 |
| 206 Partial Content | GET with Range | Requested byte range | Video streaming, resumable downloads |
4xx vs 5xx in practice — who do you blame?
4xx codes blame the client
"You did something wrong." 4xx codes should NOT be retried automatically — retrying with the same input gets the same error. The client must change the request. Common 4xx in monitoring dashboards:
- 400 — malformed JSON, validation failed
- 401 — token expired (very common — auto-refresh tokens to handle gracefully)
- 403 — permission denied
- 404 — typo in URL, deleted resource, dynamic 404 from your CMS
- 410 Gone — explicitly deleted, will never come back. Stronger than 404 for SEO removal.
- 422 — semantic validation (email format, password too short, age out of range)
- 429 — rate-limited; client must back off
5xx codes blame the server
"I tried, I failed." 5xx codes ARE typically retryable — with backoff. Most production monitors page on-call when 5xx rate exceeds 0.5%.
- 500 — unhandled exception in your code (log + fix)
- 502 Bad Gateway — your nginx couldn't reach your app server (app crashed?)
- 503 Service Unavailable — overloaded; database down; deploying. Include
Retry-After. - 504 Gateway Timeout — upstream took too long. Increase timeout or speed up upstream.
The hidden case: returning 200 with an error in the body
Common anti-pattern: 200 OK with { "error": "User not found" }. Why it's bad:
- Monitoring tools think everything is fine; alerts don't fire
- Cloudflare and CDN cache the error response
- Retry logic doesn't trigger (clients only retry on 5xx)
- Browser DevTools shows green status; users debugging are confused
Fix: use the right status. 404 for "not found", 400 for "invalid input", 500 for "I broke." The response body can also contain detail — but the status comes first.
Status codes for SEO — what Google really cares about
| Code | Google's interpretation | Long-term effect |
|---|---|---|
| 200 | Page exists, may be indexed | Indexed if content is good |
| 301 | Permanent move; transfer all signals to new URL | Old URL drops, new URL inherits backlinks & rankings |
| 302/307 | Temporary; keep original in index | Original URL stays indexed; new URL doesn't accumulate signals |
| 404 | Doesn't exist (yet); will retry crawling | Eventually drops from index after weeks of consistent 404 |
| 410 Gone | Definitively gone; deindex faster than 404 | Drops from index in days, not weeks |
| 500/503 | Temporary issue; retry later | Won't deindex unless persistent for weeks |
| 429 | Slow down crawling | Reduced crawl rate for a few days |
Common SEO mistakes around status codes:
- "Soft 404" — returning 200 OK with a "Sorry, not found" page. Google recognizes the pattern as a 404 anyway and penalizes the trick.
- 302-redirecting permanent moves — fragments link equity. Use 301.
- Returning 503 for too long during deploys — Google's crawler cuts crawl rate. Aim for sub-minute deploys.
- Returning 410 instead of 404 for deleted content — actually a positive in some cases. Use 410 when you've deliberately removed content forever (deleted blog post, removed product).
Returning HTTP status codes in 8 frameworks
Express (Node.js)
app.get('/users/:id', async (req, res) => {
const user = await db.user.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.status(200).json(user);
});
app.post('/users', async (req, res) => {
const u = await db.user.create(req.body);
res.status(201).location(`/users/${u.id}`).json(u);
});
FastAPI (Python)
from fastapi import FastAPI, HTTPException, status
@app.get("/users/{id}", status_code=status.HTTP_200_OK)
def get_user(id: int):
user = db.find_user(id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
@app.post("/users", status_code=status.HTTP_201_CREATED)
def create_user(user: UserIn):
return db.create_user(user)
Spring Boot (Java)
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userRepo.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User u) {
User saved = userRepo.save(u);
return ResponseEntity.created(URI.create("/users/" + saved.getId())).body(saved);
}
Rails (Ruby)
def show
user = User.find_by(id: params[:id])
return render(json: { error: 'Not found' }, status: :not_found) unless user
render json: user, status: :ok
end
def create
user = User.create!(user_params)
render json: user, status: :created, location: user_url(user)
end
Go (net/http)
func userHandler(w http.ResponseWriter, r *http.Request) {
user, err := db.GetUser(r.URL.Path[7:])
if err != nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(user)
}
Laravel (PHP)
public function show($id) {
$user = User::find($id);
if (!$user) return response()->json(['error' => 'Not found'], 404);
return response()->json($user, 200);
}
public function store(Request $request) {
$user = User::create($request->validated());
return response()->json($user, 201)
->header('Location', "/users/{$user->id}");
}
Cloudflare Workers / Edge runtimes
export default {
async fetch(req, env) {
const u = await env.DB.get(extractId(req));
if (!u) return new Response('Not found', { status: 404 });
return new Response(JSON.stringify(u), {
status: 200,
headers: { 'content-type': 'application/json' },
});
},
};
Bash / curl (testing)
# Show only the status code (great for monitoring scripts)
curl -s -o /dev/null -w '%{http_code}\n' https://example.com
# Show full headers
curl -I https://example.com
# Follow redirects and report all hops
curl -ILs https://example.com
# Fail bash script if status is not 2xx
curl --fail-with-body https://example.com/api
Status code best practices
- Use the most specific code. 422 for validation errors beats 400 for everything. 410 for permanently-deleted content beats 404. Specificity helps clients build better error handling.
- Never return 200 with an error. Status codes are how every layer of the stack makes decisions. Lying about success breaks monitoring, retries, caching.
- Add
Retry-Afteron 429 and 503. Tells clients exactly how long to wait. Servers and CDNs (Cloudflare) honor it automatically. - Don't leak stack traces in 500 responses. Generic "Internal Server Error" + log the real details server-side. Stack traces in production are a security hole.
- Use 301 for permanent URL changes; 302 for temporary. Confusing them fragments SEO equity.
- Return a structured error body with the status code:
{ "error": { "code": "USER_NOT_FOUND", "message": "...", "details": {} } }. Lets clients programmatically handle specific errors. - Set proper headers alongside status codes.
Location:on 201/3xx,WWW-Authenticate:on 401,Retry-After:on 429/503,Allow:on 405. - Use 405 Method Not Allowed for "this endpoint exists, but you can't POST to it." Include
Allow: GET, PUTin response. - Distinguish 401 (auth) from 403 (permissions). Critical for client retry logic — 401 prompts re-login, 403 doesn't.
- Don't use 200 for "delete succeeded". Use 204 No Content. Saves bandwidth, signals intent.