The one-code-per-situation reference table
Most "which status code?" arguments come down to a handful of near-collisions. This table gives the single recommended code for each common situation, the runner-up code teams also use, and the header or body detail that makes the choice unambiguous. Codes marked with the RFC that defines them are stable and safe to rely on: 9110 is the core HTTP semantics spec (2022), 6585 adds 429, and 4918 (WebDAV) is where 422 and 207 originate.
| Situation | Use this code | Also seen | The detail that decides it |
|---|---|---|---|
| GET returns existing data | 200 OK | — | Body present; cacheable |
| POST creates a new resource | 201 Created | 200 | Must include Location: to the new resource |
| Request accepted, work runs async | 202 Accepted | 201 | Location: points to a job/status URL, not the resource |
| Success but no body to send (DELETE, PUT) | 204 No Content | 200 | No body at all — clients must not parse one |
| Client not logged in / bad token | 401 Unauthorized | — | Send WWW-Authenticate; re-auth may fix it |
| Logged in but lacks permission | 403 Forbidden | 404 | Re-auth won't help; return 404 to hide existence |
| Well-formed request, resource missing | 404 Not Found | 410 | Use 410 Gone if it existed and was deleted |
| Malformed syntax (bad JSON, bad ID type) | 400 Bad Request | 422 | The parser failed before validation ran |
| Valid syntax, fails business rules | 422 Unprocessable Entity | 400 | Email formatted correctly but already taken |
| Write conflicts with current state | 409 Conflict | 412 | Use 412 Precondition Failed with If-Match ETags |
| Client exceeded rate limit | 429 Too Many Requests | 503 | Always send Retry-After; 503 implies server fault |
| Unexpected server bug | 500 Internal Server Error | — | Log a request_id; never leak stack traces |
| Upstream/proxy returned garbage | 502 Bad Gateway | 504 | 504 if upstream timed out instead of erroring |
| Down for maintenance / overloaded | 503 Service Unavailable | 500 | Send Retry-After; it's explicitly temporary |
| Batch with mixed success/failure | 207 Multi-Status | 200 | Per-item status codes live in the body |
Which should I use when the code is ambiguous? Pick the code that lets the client decide its next action without reading the body: 401 says "re-authenticate," 403 says "stop," 429 says "wait Retry-After seconds," 409 says "re-fetch and retry," and 500 says "report a bug." If two codes would trigger the same client behavior, pick the more common one and document it.
The 401 vs 403 vs 404 decision, visualized
The single most common status-code mistake is collapsing authentication, authorization, and existence into one code. Walk a request left-to-right: each gate answers one question, and the first gate that fails picks the code.
The 404-to-hide trick matters for security: if returning 403 on a resource the user can't see would confirm that the resource exists, return 404 instead so unauthorized clients can't enumerate private records. Document whichever convention you pick — clients need consistency more than they need theoretical purity.
Core RESTful Status Code Rules
2xx Success - Operation Succeeded
200 OK: Successful request with response body
GET /users/{id}
200 OK
{"id": 123, "name": "John", "email": "john@example.com"}
PUT /users/{id}
200 OK
{"id": 123, "name": "John Updated", "email": "john@example.com"}
201 Created: Successful resource creation
POST /users
201 Created
Location: /users/{id}
{"id": 124, "name": "Jane", "email": "jane@example.com"}
Key: Include Location header pointing to new resource
202 Accepted: Request accepted for asynchronous processing
POST /reports/generate
202 Accepted
Location: /reports/jobs/789
{"status": "processing", "job_id": "789"}
204 No Content: Successful request with no response body
DELETE /users/{id}
204 No Content
PUT /settings
204 No Content
Rule: Use appropriate 2xx code to clearly signal success type.
3xx Redirection - Client Action Required
301 Moved Permanently: Resource permanently relocated
GET https://api.example.com/v1/users
301 Moved Permanently
Location: https://api.example.com/v2/users
[Client should update to use new URL]
302 Found: Temporary redirect
GET https://example.com/users
302 Found
Location: https://api.example.com/users
[Client may update to new location]
304 Not Modified: Resource unchanged since last request
GET /data HTTP/1.1
If-None-Match: "abc123"
304 Not Modified
[Client uses cached copy]
307 Temporary Redirect: Like 302, but preserves HTTP method
POST /form
307 Temporary Redirect
Location: /form-processor
[Browser re-POSTs to new location, not GET]
Rule: Use 3xx when client action needed to complete request.
4xx Client Error - Client's Fault
400 Bad Request: Malformed request
POST /users
400 Bad Request
{"error": "Invalid JSON: missing closing brace"}
401 Unauthorized: Missing or invalid authentication
GET https://api.example.com/admin
401 Unauthorized
{"error": "Missing authentication token"}
403 Forbidden: Authenticated but unauthorized
GET https://api.example.com/admin
403 Forbidden
{"error": "User role 'viewer' lacks access to admin"}
404 Not Found: Resource doesn't exist
GET /users/{nonexistent-id}
404 Not Found
{"error": "User not found"}
409 Conflict: Request conflicts with current state
PUT /document/123
409 Conflict
{"error": "Document was modified; refresh and retry"}
422 Unprocessable Entity: Valid format, invalid content
POST /users
422 Unprocessable Entity
{"error": "Email already in use"}
429 Too Many Requests: Rate limit exceeded
GET https://api.example.com/search
429 Too Many Requests
Retry-After: 60
{"error": "Rate limit exceeded"}
Rule: 4xx indicates client made a mistake; client must correct and retry.
5xx Server Error - Server's Fault
500 Internal Server Error: Unexpected server error
GET /data
500 Internal Server Error
{"error": "Internal error occurred"}
502 Bad Gateway: Upstream service returned invalid response
GET https://api.example.com/data
502 Bad Gateway
{"error": "Upstream service error"}
503 Service Unavailable: Server temporarily unavailable
GET https://api.example.com/data
503 Service Unavailable
Retry-After: 300
{"error": "Server maintenance in progress"}
504 Gateway Timeout: Upstream service too slow
GET https://api.example.com/data
504 Gateway Timeout
{"error": "Upstream service timeout"}
Rule: 5xx indicates server error; client might retry later.
RESTful Operation Patterns
GET - Reading Resources
GET /users → 200 OK (list of users)
GET /users/{id} → 200 OK (single user)
GET /users/{nonexistent} → 404 Not Found
GET /users?updated-since=date → 304 Not Modified (if unchanged)
GET /users (auth required) → 401 Unauthorized or 403 Forbidden
Best practice: Always use 200 OK for successful GET.
POST - Creating Resources
POST /users → 201 Created + Location header
POST /users (invalid data) → 400 Bad Request or 422 Unprocessable
POST /users (duplicate) → 409 Conflict
POST /async-job → 202 Accepted + Location for tracking
POST /protected → 401 Unauthorized or 403 Forbidden
Best practice: Return 201 with Location header pointing to created resource.
PUT - Replacing Resources
PUT /users/{id} → 200 OK (return updated resource)
PUT /users/{id} → 204 No Content (if not returning body)
PUT /users/{id} (not found) → 404 Not Found or 201 Created (for create)
PUT /users/{id} (conflict) → 409 Conflict (if versioned)
PUT /protected → 401 or 403
Best practice: Return 200 OK with updated resource or 204 No Content.
PATCH - Partial Updates
PATCH /users/{id} → 200 OK (return updated resource)
PATCH /users/{id} → 204 No Content
PATCH /users/{id} (conflict) → 409 Conflict
PATCH /users/{id} (invalid) → 400 Bad Request or 422 Unprocessable
Best practice: Similar to PUT; return updated resource or 204.
DELETE - Deleting Resources
DELETE /users/{id} → 204 No Content
DELETE /users/{id} → 200 OK (with deletion confirmation)
DELETE /users/{nonexistent} → 404 Not Found
DELETE /users/{id} (conflict) → 409 Conflict (has dependencies)
DELETE /protected → 401 or 403
Best practice: Return 204 No Content (safest) or 200 OK with confirmation.
Status Code Decision Tree
START: Client makes request
Does request succeed?
├─ YES
│ └─ Is this a resource creation (POST)?
│ ├─ YES → 201 Created (with Location header)
│ ├─ NO → Is there response content?
│ ├─ YES → 200 OK
│ ├─ NO → 204 No Content
│ ├─ Is this async processing?
│ └─ YES → 202 Accepted (with tracking Location)
│
├─ NO
│ ├─ Is the request format bad?
│ │ └─ YES → 400 Bad Request
│ │
│ ├─ Is this missing/invalid authentication?
│ │ └─ YES → 401 Unauthorized
│ │
│ ├─ Is user authenticated but not authorized?
│ │ └─ YES → 403 Forbidden
│ │
│ ├─ Does the resource not exist?
│ │ └─ YES → 404 Not Found
│ │
│ ├─ Does content validation fail?
│ │ └─ YES → 422 Unprocessable Entity
│ │
│ ├─ Does the request conflict with current state?
│ │ └─ YES → 409 Conflict
│ │
│ ├─ Did the server have an unexpected error?
│ │ └─ YES → 500 Internal Server Error
│ │
│ ├─ Is the server temporarily unavailable?
│ │ └─ YES → 503 Service Unavailable
Error Response Format Best Practices
Consistent Error Structure
{
"error": "human-readable message",
"code": "MACHINE_READABLE_CODE",
"status": 400,
"timestamp": "2025-01-31T10:00:00Z",
"request_id": "req-abc-123",
"details": {
"field": "email",
"reason": "Email already registered"
}
}
Validation Error Response
{
"error": "Validation failed",
"code": "VALIDATION_ERROR",
"status": 422,
"errors": [
{
"field": "email",
"message": "Invalid email format"
},
{
"field": "age",
"message": "Must be 18 or older"
}
]
}
Consistency Across Endpoints
Bad - Inconsistent error formats:
GET /users → {"error": "Not found"}
POST /users → {"message": "User not found"}
DELETE /users → {"error_message": "User not found"}
Good - Consistent error format:
GET /users → {"error": "User not found", "code": "NOT_FOUND"}
POST /users → {"error": "User not found", "code": "NOT_FOUND"}
DELETE /users → {"error": "User not found", "code": "NOT_FOUND"}
Handling Common Scenarios
Bulk Operations
For bulk operations, multiple status codes may apply:
POST https://api.example.com/users/bulk
[create 10 users, 7 succeed, 3 fail with duplicates]
Option 1 - Partial Success:
202 Accepted
{
"created": 7,
"failed": 3,
"errors": [
{"index": 2, "error": "Duplicate email"}
]
}
Option 2 - Fail Completely:
422 Unprocessable Entity
{
"error": "Bulk operation partially failed",
"details": [...]
}
Recommendation: Document behavior clearly. Partial success (202) more user-friendly.
Long-Running Operations
POST https://api.example.com/export/large-dataset
202 Accepted
Location: https://api.example.com/jobs/{jobId}
{
"status": "processing",
"job_id": "abc-123",
"progress": 0
}
[Client polls Job endpoint]
GET https://api.example.com/jobs/{jobId}
200 OK
{
"status": "processing",
"progress": 45
}
[When complete]
GET https://api.example.com/jobs/{jobId}
200 OK
{
"status": "completed",
"progress": 100,
"download_url": "https://cdn.example.com/exports/dataset-abc-123.csv"
}
Conditional Requests
GET /data HTTP/1.1
If-None-Match: "abc123"
Server: Is data unchanged?
├─ YES → 304 Not Modified [no body]
└─ NO → 200 OK [with current data]
Rate Limiting
GET https://api.example.com/search
429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706779200
{"error": "Rate limit exceeded"}
API Versioning with Status Codes
Deprecating API Versions
GET https://api.example.com/v1/users (old)
301 Moved Permanently
Location: https://api.example.com/v2/users
[Tells clients to migrate]
vs.
GET https://api.example.com/v1/users (deprecated)
200 OK
Deprecation: true
Sunset: Sun, 31 Dec 2025 23:59:59 GMT
[Warns about upcoming removal]
Use 301: When old version should never be used Use 200 with warnings: When deprecation window needed
Testing Status Code Implementation
Test Matrix
Operation | Normal | Auth Fail | Conflict | Not Found
GET | 200 | 401/403 | N/A | 404
POST (create)| 201 | 401/403 | 409 | N/A
PUT | 200 | 401/403 | 409 | 404
PATCH | 200 | 401/403 | 409 | 404
DELETE | 204 | 401/403 | 409 | 404
Common Test Cases
def test_api_status_codes():
# GET existing resource
assert get("/users/{id}").status == 200
# GET non-existing resource
assert get("/users/{nonexistent}").status == 404
# POST create resource
response = post("/users", {"name": "John"})
assert response.status == 201
assert "Location" in response.headers
# POST with auth failure
assert post("/users", {}, headers={}).status == 401
# DELETE
assert delete("/users/{id}").status == 204
# Conditional request
response1 = get("/data")
etag = response1.headers["ETag"]
response2 = get("/data", headers={"If-None-Match": etag})
assert response2.status == 304
Conclusion
Proper HTTP status code usage in RESTful APIs is fundamental to creating predictable, usable services. By following these practices:
- Use appropriate 2xx codes for success (200, 201, 202, 204)
- Use 3xx for redirects and conditional responses
- Use 4xx for client errors (400, 401, 403, 404, 409, 422, 429)
- Use 5xx for server errors
- Provide consistent error response formats
- Include helpful response headers
- Document status code meanings
APIs that use status codes properly are more usable, more testable, and can be automated more effectively. Invest time getting this right from the beginning of API design.