Every web request gets a three-digit response code. Knowing what each one means — and when to use each one in your own APIs — is fundamental to web development.
Quick reference
The codes you'll encounter most often.
| Code | Name | Meaning |
|---|---|---|
200 |
OK | Request succeeded |
201 |
Created | Resource created successfully |
204 |
No Content | Success with no response body |
301 |
Moved Permanently | URL changed forever |
302 |
Found | Temporary redirect |
304 |
Not Modified | Use cached version |
400 |
Bad Request | Client sent invalid data |
401 |
Unauthorized | Authentication required |
403 |
Forbidden | Authenticated but not allowed |
404 |
Not Found | Resource doesn't exist |
405 |
Method Not Allowed | Wrong HTTP verb |
409 |
Conflict | State conflict (e.g. duplicate) |
422 |
Unprocessable Entity | Validation failed |
429 |
Too Many Requests | Rate limit exceeded |
500 |
Internal Server Error | Server-side bug |
502 |
Bad Gateway | Upstream server failed |
503 |
Service Unavailable | Server down or overloaded |
1xx — Informational
Rarely seen in practice. The server is acknowledging the request and asking the client to continue.
| Code | Name | When you see it |
|---|---|---|
100 |
Continue | Server received request headers; client should send the body |
101 |
Switching Protocols | Server agrees to upgrade (e.g. HTTP → WebSocket) |
103 |
Early Hints | Preload hints sent before the final response |
101 Switching Protocols is the code you get when upgrading to WebSocket:
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
2xx — Success
The request was received, understood, and accepted.
200 OK
The standard success response. Return 200 for successful GET, PUT, and PATCH requests.
// Express.js
app.get('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.status(200).json(user); // or just res.json(user)
});
201 Created
Return after successfully creating a resource. Include a Location header pointing to the new resource.
app.post('/users', async (req, res) => {
const user = await db.users.create(req.body);
res
.status(201)
.setHeader('Location', `/users/${user.id}`)
.json(user);
});
204 No Content
Success, but nothing to return. Use for DELETE operations and PATCH when you don't return the updated resource.
app.delete('/users/:id', async (req, res) => {
await db.users.delete(req.params.id);
res.status(204).end(); // no body
});
206 Partial Content
Used for range requests — streaming large files, resumable downloads, video seeking.
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/146515
Content-Length: 1024
3xx — Redirection
The client must take additional action to complete the request.
301 Moved Permanently
The URL has changed permanently. Search engines update their index; browsers cache the redirect. Use when you rename a URL forever.
// Redirect old URL to new
app.get('/old-path', (req, res) => {
res.redirect(301, '/new-path');
});
302 Found (Temporary Redirect)
The URL has temporarily moved. Browsers follow it but don't update bookmarks. Search engines don't transfer link juice.
303 See Other
After a POST, redirect to a GET endpoint to prevent form resubmission. This is the correct code for the Post/Redirect/Get pattern.
app.post('/submit-form', async (req, res) => {
await processForm(req.body);
res.redirect(303, '/thank-you'); // GET /thank-you
});
304 Not Modified
The resource hasn't changed since the client's cached version. The browser uses the cache instead of downloading again.
app.get('/static/data.json', (req, res) => {
const etag = computeETag(data);
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.setHeader('ETag', etag).json(data);
});
307 Temporary Redirect / 308 Permanent Redirect
Like 302/301, but the client must keep the same HTTP method. Use these instead of 301/302 for POST redirects that shouldn't change to GET.
| Code | Permanent? | Method preserved? |
|---|---|---|
| 301 | Yes | No — POST may become GET |
| 302 | No | No — POST may become GET |
| 307 | No | Yes |
| 308 | Yes | Yes |
4xx — Client Errors
The client made an invalid request. The problem is on the client side.
400 Bad Request
Generic client error — malformed JSON, missing required fields, invalid parameter types.
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.post('/users')
def create_user():
data = request.get_json()
if not data or 'email' not in data:
return jsonify({'error': 'email is required'}), 400
# ...
401 Unauthorized
Despite the name, this means unauthenticated — no valid credentials were provided. Always include a WWW-Authenticate header.
@app.get('/profile')
def profile():
token = request.headers.get('Authorization')
if not token:
return jsonify({'error': 'Authentication required'}), 401
# verify token...
403 Forbidden
The client is authenticated but doesn't have permission. Don't reveal whether the resource exists if that's sensitive — return 404 instead.
@app.delete('/posts/<int:post_id>')
def delete_post(post_id):
post = Post.query.get_or_404(post_id)
if post.author_id != current_user.id:
return jsonify({'error': 'Access denied'}), 403
post.delete()
return '', 204
404 Not Found
The resource doesn't exist at this URL. Also use when you want to hide the existence of a resource from unauthorized users.
405 Method Not Allowed
The URL exists but doesn't support this HTTP method. Include an Allow header listing valid methods.
HTTP/1.1 405 Method Not Allowed
Allow: GET, POST
409 Conflict
The request conflicts with the current state — duplicate entry, optimistic locking failure, version mismatch.
// Go example
func createUser(w http.ResponseWriter, r *http.Request) {
var user User
json.NewDecoder(r.Body).Decode(&user)
if userExists(user.Email) {
w.WriteHeader(http.StatusConflict) // 409
json.NewEncoder(w).Encode(map[string]string{
"error": "email already registered",
})
return
}
// create user...
}
410 Gone
Like 404, but permanent — the resource existed and was intentionally deleted. Useful for telling crawlers to remove the URL from their index.
422 Unprocessable Entity
The request is syntactically valid (parseable JSON) but semantically invalid — field validation failures. Preferred over 400 for validation errors in REST APIs.
func createUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
w.WriteHeader(http.StatusBadRequest) // 400 — can't parse
return
}
if errors := validate(user); len(errors) > 0 {
w.WriteHeader(http.StatusUnprocessableEntity) // 422 — invalid data
json.NewEncoder(w).Encode(map[string]any{"errors": errors})
return
}
}
429 Too Many Requests
Rate limit exceeded. Include Retry-After to tell clients when they can try again.
<?php
function checkRateLimit(string $ip): bool {
$key = "rate:$ip";
$count = (int)$redis->get($key);
if ($count >= 100) {
http_response_code(429);
header('Retry-After: 60');
echo json_encode(['error' => 'Too many requests']);
return false;
}
$redis->incr($key);
$redis->expire($key, 60);
return true;
}
5xx — Server Errors
Something went wrong on the server side. The client did nothing wrong.
500 Internal Server Error
The catch-all for unhandled exceptions. Don't leak stack traces or internal details to the client.
// Express error handler
app.use((err, req, res, next) => {
console.error(err); // log full error server-side
res.status(500).json({
error: 'Something went wrong' // never leak err.message to client
});
});
502 Bad Gateway
Your server got an invalid response from an upstream service (database, microservice, external API). The issue is between your server and something behind it.
503 Service Unavailable
Server is temporarily unable to handle requests — maintenance mode, overload, dependency down. Return Retry-After to tell clients when to retry.
HTTP/1.1 503 Service Unavailable
Retry-After: 3600
Content-Type: application/json
{"error": "Service temporarily unavailable. Try again in 1 hour."}
504 Gateway Timeout
Your server waited too long for an upstream response. Common cause: slow database query, third-party API that doesn't respond.
Checking status codes in code
JavaScript (fetch)
const res = await fetch('/api/users/42');
if (!res.ok) { // res.ok is true for 200–299
const err = await res.json();
throw new Error(err.message);
}
const user = await res.json();
Python (requests)
import requests
r = requests.get('https://api.example.com/users/42')
# Raises HTTPError for 4xx and 5xx
r.raise_for_status()
# Or check manually
if r.status_code == 404:
print('Not found')
elif r.status_code == 200:
user = r.json()
Go (net/http)
resp, err := http.Get("https://api.example.com/users/42")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK: // 200
json.NewDecoder(resp.Body).Decode(&user)
case http.StatusNotFound: // 404
log.Println("not found")
case http.StatusUnauthorized: // 401
log.Println("auth required")
default:
log.Printf("unexpected status: %d", resp.StatusCode)
}
PHP (cURL)
<?php
$ch = curl_init('https://api.example.com/users/42');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
match(true) {
$status === 200 => $user = json_decode($body, true),
$status === 404 => throw new NotFoundException(),
$status >= 500 => throw new ServerException("Status: $status"),
default => throw new ApiException("Status: $status"),
};
Status code decision guide
When building an API, use this flowchart logic:
Did the request succeed?
├── Yes → 2xx
│ ├── Created a resource? → 201 (+ Location header)
│ ├── No response body? → 204
│ └── Otherwise → 200
└── No → was it the client's fault?
├── Yes → 4xx
│ ├── Not authenticated? → 401
│ ├── Not authorized? → 403
│ ├── Not found? → 404
│ ├── Duplicate/conflict? → 409
│ ├── Validation error? → 422
│ ├── Rate limited? → 429
│ └── Other bad input → 400
└── No (server fault) → 5xx
├── Upstream failed? → 502
├── Timed out? → 504
├── Down for maintenance? → 503
└── Unexpected error → 500
6 common mistakes
1. Using 200 for errors. Never return { "success": false } with a 200 status. Use the appropriate 4xx or 5xx code so clients can handle errors without parsing the body.
2. Confusing 401 and 403. Use 401 when credentials are missing or invalid (not logged in). Use 403 when the user is authenticated but lacks permission (logged in but not allowed).
3. Using 404 for everything. Returning 404 for "wrong password" or "user banned" hides useful information from legitimate clients while not actually hiding it from attackers. Use specific codes.
4. Leaking error details in 500 responses. Never send stack traces, SQL errors, or file paths to the client. Log them server-side; send a generic message to the client.
5. Forgetting Retry-After on 429 and 503. Without this header, clients don't know when it's safe to retry and may hammer the server.
6. Redirecting POST to GET with 301/302. Use 307/308 when you need to redirect while preserving the HTTP method, or use 303 for the Post/Redirect/Get pattern.
FAQ
What's the difference between 401 and 403?
401 means "you haven't identified yourself" (no token, expired token, invalid token). 403 means "I know who you are, but you're not allowed to do this." Think of it as: 401 = no badge, 403 = badge doesn't grant access.
Should I return 404 or 403 for a resource a user can't access?
It depends on whether revealing the resource's existence is sensitive. If you're fine saying "this exists but you can't see it," use 403. If knowing the resource exists would be a security issue (e.g. private user profiles), return 404 for everyone who doesn't have access.
When should I use 400 vs 422?
Use 400 when the request is structurally broken — unparseable JSON, wrong Content-Type, missing required headers. Use 422 when the request parses correctly but the data fails validation — invalid email format, negative price, future date required.
Why does my browser show ERR_EMPTY_RESPONSE instead of a status code?
The server closed the connection without sending an HTTP response — a connection-level problem, not an HTTP error. The server likely crashed before sending headers, the connection timed out at the TCP level, or there's a firewall/proxy issue.
What does it mean when an API returns 200 with "error": true in the body?
It's an anti-pattern sometimes called "200 OK with error body." The API should use proper 4xx/5xx codes instead. Most HTTP clients and monitoring tools key on the status code, not the body.
Is there a status code for "I'm a teapot"?
Yes — 418 I'm a Teapot. Defined in RFC 2324 as an April Fool's joke, it means the server refuses to brew coffee because it is a teapot. Some APIs use it for requests they intentionally won't fulfil.