HTTP Status Codes Cheat Sheet
This HTTP status codes cheat sheet covers 28 sections: every 1xx to 5xx code with the headers it needs, which code to return in an API, 401 vs 403, 400 vs 422 and 301 vs 308, problem details error bodies, caching and 304, 429 and retries, redirects and SEO, curl and fetch, eleven frameworks, nginx and Apache, and how to fix 502, 503 and 504. Search it, filter it by level, and check a real response or redirect chain without leaving the page.
404 Not Found
Nothing matches that search. Try a code such as 404, a word such as redirect or retry, or a header such as Location. Several words are combined, so every one of them has to match. Keys: / focuses the search, Esc clears it, t switches the theme.
Toolbox & Basics
look up any code, check a response, and read a status line
Builders: Error Body, Redirect Rules, Status in Code, Error Pages
Anatomy of a Status Line
A response starts with the protocol, the code and a reason phrase
HTTP/1.1 404 Not Found Content-Type: text/html; charset=utf-8 Content-Length: 1245 Cache-Control: max-age=60 <!doctype html>...
The first digit is the class
| Class | Meaning | What the client does |
|---|---|---|
| 1xx | informational, an interim response | keeps waiting for the final response |
| 2xx | success | uses the response |
| 3xx | redirection | follows Location or uses its cache |
| 4xx | client error | fixes the request before trying again |
| 5xx | server error | may retry later, the request itself can be fine |
An unknown code falls back to its class
HTTP/1.1 299 Something New # a client that does not know 299 treats it as 200 # an unknown 4xx is handled like 400, an unknown 5xx like 500
See the Code From the Terminal
Print only the status code
curl -s -o /dev/null -w "%{http_code}\n" https://httpbingo.org/status/404
Show the status line and headers
curl -sI https://httpbingo.org/status/429 | head -3
Follow redirects and see every hop
curl -sIL "https://httpbingo.org/redirect-to?url=/status/204&status_code=301" | grep -iE "^(HTTP|location)"
The Five Classes
1xx informational, 2xx success, 3xx redirection, 4xx client errors, 5xx server errors at a glance
The Codes You Will See Most
Twenty codes cover nearly every real response
| Code | Name | In one line |
|---|---|---|
| 200 | OK | the request worked and the body is the result |
| 201 | Created | a new resource exists; Location points to it |
| 204 | No Content | it worked and there is nothing to send back |
| 301 | Moved Permanently | the URL changed for good; update links |
| 302 | Found | look at another URL for now |
| 304 | Not Modified | your cached copy is still good |
| 307 | Temporary Redirect | like 302, but repeat the same method and body |
| 308 | Permanent Redirect | like 301, but repeat the same method and body |
| 400 | Bad Request | the request is malformed |
| 401 | Unauthorized | you are not authenticated; log in or send a token |
| 403 | Forbidden | you are authenticated but not allowed |
| 404 | Not Found | nothing at this URL |
| 405 | Method Not Allowed | the URL exists, the method does not; see Allow |
| 409 | Conflict | the request clashes with the current state |
| 422 | Unprocessable Content | well formed, but the data fails validation |
| 429 | Too Many Requests | slow down; see Retry-After |
| 500 | Internal Server Error | the server crashed or failed unexpectedly |
| 502 | Bad Gateway | a proxy got a bad answer from the upstream server |
| 503 | Service Unavailable | overloaded or in maintenance; try later |
| 504 | Gateway Timeout | a proxy waited too long for the upstream server |
Which Side Is at Fault
4xx blames the request, 5xx blames the server
| Situation | Class | Retry the same request? |
|---|---|---|
| the body is invalid JSON | 4xx (400) | no, fix it first |
| the token expired | 4xx (401) | after refreshing the token |
| the database is down | 5xx (503) | yes, with backoff |
| a bug threw an exception | 5xx (500) | maybe, if the method is idempotent |
| too many requests | 4xx (429) | yes, after Retry-After |
Classes by number range
| Range | Registered codes | Notes |
|---|---|---|
| 100 to 199 | 100, 101, 102, 103, 104 | 104 is a temporary registration |
| 200 to 299 | 200 to 208, 226 | 207, 208 and 226 come from WebDAV and delta encoding |
| 300 to 399 | 300 to 308 | 305 is deprecated and 306 is unused |
| 400 to 499 | 400 to 418, 421 to 426, 428, 429, 431, 451 | 418 is reserved, not assigned |
| 500 to 599 | 500 to 508, 510, 511 | 510 is obsoleted |
1xx Informational
100 Continue, 101 Switching Protocols, 102 Processing, 103 Early Hints, 104 Upload Resumption
100 Continue
The client asks before sending a large body
PUT /uploads/video.mp4 HTTP/1.1 Host: api.example.com Content-Length: 734003200 Expect: 100-continue HTTP/1.1 100 Continue HTTP/1.1 201 Created Location: /uploads/video.mp4
curl adds the header for large uploads
curl -T video.mp4 https://api.example.com/uploads/ curl -H "Expect:" -T video.mp4 https://api.example.com/uploads/ # never wait curl --expect100-timeout 0.5 -T video.mp4 https://api.example.com/uploads/
101 Switching Protocols
Upgrading an HTTP/1.1 connection to a WebSocket
GET /chat HTTP/1.1 Host: example.com Connection: Upgrade Upgrade: websocket Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 HTTP/1.1 101 Switching Protocols Connection: Upgrade Upgrade: websocket Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
102 and 103
102 Processing is deprecated
HTTP/1.1 102 Processing
103 Early Hints lets the browser start loading assets early
HTTP/1.1 103 Early Hints Link: </styles/main.css>; rel=preload; as=style Link: <https://cdn.example.com>; rel=preconnect HTTP/1.1 200 OK Content-Type: text/html Link: </styles/main.css>; rel=preload; as=style
104 Upload Resumption Supported is provisional
HTTP/1.1 104 Upload Resumption Supported Location: https://example.com/upload/b530ce8ff
2xx Success
200, 201, 202, 204, 206 and the WebDAV success codes
200 OK
The default success, with a body
GET /users/42 HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 42, "name": "Ana"}
A 200 that reports an error is a bug
HTTP/1.1 200 OK
Content-Type: application/json
{"success": false, "error": "User not found"}
201 Created
Return the new resource and its URL
POST /users HTTP/1.1
Content-Type: application/json
{"name": "Ana"}
HTTP/1.1 201 Created
Location: /users/43
Content-Type: application/json
{"id": 43, "name": "Ana"}
202 Accepted
Accept work that finishes later
POST /reports HTTP/1.1
HTTP/1.1 202 Accepted
Location: /reports/jobs/9f3a
Retry-After: 10
{"status": "queued", "check": "/reports/jobs/9f3a"}
204 No Content
Success with nothing to return
DELETE /users/43 HTTP/1.1 HTTP/1.1 204 No Content
206 Partial Content
Range requests resume downloads and stream video
GET /video.mp4 HTTP/1.1 Range: bytes=0-1048575 HTTP/1.1 206 Partial Content Content-Range: bytes 0-1048575/73400320 Content-Length: 1048576 Accept-Ranges: bytes
Ask for the last bytes of a file
curl -s -r -16 -D - -o /dev/null https://httpbingo.org/range/1024 | grep -iE "^(HTTP|content-range)"
Rare 2xx Codes
203, 205, 207, 208 and 226
| Code | Name | Where it appears |
|---|---|---|
| 203 | Non-Authoritative Information | a proxy changed a 200 response from the origin |
| 205 | Reset Content | the client should clear the form that sent the request |
| 207 | Multi-Status | WebDAV: an XML body holds a status per resource |
| 208 | Already Reported | WebDAV bindings: a member was listed already |
| 226 | IM Used | delta encoding: the body is a diff (RFC 3229) |
3xx Redirection
301, 302, 303, 307, 308, 304 and which method a redirect keeps
The Redirect Codes Side by Side
Permanent or temporary, and does the method survive
| Code | Permanent | POST stays POST | Use it for |
|---|---|---|---|
| 301 Moved Permanently | yes | no, clients may switch to GET | a page or site moved for good |
| 308 Permanent Redirect | yes | yes | a moved API endpoint that receives POST or PUT |
| 302 Found | no | no, clients may switch to GET | a short-term move of a page |
| 307 Temporary Redirect | no | yes | sending a POST somewhere else temporarily |
| 303 See Other | no | always becomes GET | showing a result page after a form POST |
Every redirect needs a Location header
HTTP/1.1 301 Moved Permanently Location: https://example.com/new-page Content-Length: 0
303 After a Form
Post, redirect, get
POST /orders HTTP/1.1 Content-Type: application/x-www-form-urlencoded item=42&qty=1 HTTP/1.1 303 See Other Location: /orders/8812
304 Not Modified
A conditional GET answered from the cache
GET /logo.png HTTP/1.1 If-None-Match: "33a64df5" HTTP/1.1 304 Not Modified ETag: "33a64df5" Cache-Control: max-age=3600
Redirect Loops
curl stops after too many hops
curl -sL --max-redirs 5 -o /dev/null -w "%{http_code} after %{num_redirects} hops\n" https://httpbingo.org/redirect/10; echo "exit $?"
The proxy loop, and its fix
# the app sees http because TLS ends at the load balancer, so it redirects forever
if ($http_x_forwarded_proto = "http") {
return 301 https://$host$request_uri;
}
Rare and Retired 3xx
300, 305 and 306
| Code | Name | Status |
|---|---|---|
| 300 | Multiple Choices | valid, but browsers do not offer the choice; rarely used |
| 305 | Use Proxy | deprecated: clients ignore it for security reasons |
| 306 | (Unused) | was Switch Proxy in a draft; reserved, never send it |
400 to 409
bad request, authentication, permissions, not found, wrong method
400 Bad Request
The request cannot be understood
POST /users HTTP/1.1
Content-Type: application/json
{"name": "Ana",}
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{"title": "Malformed JSON", "status": 400, "detail": "Unexpected token } at position 15"}
401 Unauthorized or 403 Forbidden
401 means who are you, 403 means you may not
GET /admin/reports HTTP/1.1 HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer realm="api", error="invalid_token" GET /admin/reports HTTP/1.1 Authorization: Bearer eyJhbGciOi... HTTP/1.1 403 Forbidden
Hide a resource's existence with 404
GET /orgs/acme/secret-project HTTP/1.1 Authorization: Bearer eyJhbGciOi... HTTP/1.1 404 Not Found
404 Not Found
Nothing at this URL, maybe ever
GET /products/does-not-exist HTTP/1.1 HTTP/1.1 404 Not Found Content-Type: text/html Cache-Control: max-age=300
405 Method Not Allowed
The URL exists, the method does not
DELETE /users HTTP/1.1 HTTP/1.1 405 Method Not Allowed Allow: GET, POST
Other 40x Codes
402, 406, 407, 408
| Code | Name | When |
|---|---|---|
| 402 | Payment Required | reserved by the spec; some APIs use it for unpaid plans or exhausted credit |
| 406 | Not Acceptable | nothing matches the Accept header, for example Accept: application/xml on a JSON-only API |
| 407 | Proxy Authentication Required | a proxy wants credentials; comes with Proxy-Authenticate |
| 408 | Request Timeout | the client was too slow sending the request; the server closes the connection |
409 Conflict
The request clashes with the current state
POST /users HTTP/1.1
Content-Type: application/json
{"email": "ana@example.com"}
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{"title": "Email already registered", "status": 409}
410 to 418
gone, preconditions, size limits, media types, ranges and the teapot
410 Gone
Removed on purpose, not coming back
GET /promo/summer-2024 HTTP/1.1 HTTP/1.1 410 Gone
412 Precondition Failed
Someone else changed it first
PUT /docs/7 HTTP/1.1
If-Match: "v41"
Content-Type: application/json
{"title": "New title"}
HTTP/1.1 412 Precondition Failed
ETag: "v42"
Size and Format Limits
413, 414, 415 and 431
| Code | Name | Typical cause |
|---|---|---|
| 413 | Content Too Large | upload over the limit, often nginx client_max_body_size, 1 MB by default |
| 414 | URI Too Long | a huge query string; move the data into a POST or QUERY body |
| 415 | Unsupported Media Type | wrong Content-Type, for example sending form data to a JSON endpoint |
| 431 | Request Header Fields Too Large | too many or oversized cookies or headers |
Each library spells the reason phrase its own way
node -e "const h=require('http'); console.log(h.STATUS_CODES[413], '|', h.STATUS_CODES[422])"
python -c "from http import HTTPStatus as S; print(S(413).phrase, '|', S(422).phrase)"
416, 417 and 418
Range Not Satisfiable states the real size
GET /file.zip HTTP/1.1 Range: bytes=9000000- HTTP/1.1 416 Range Not Satisfiable Content-Range: bytes */1048576
418 is a joke the spec keeps reserved
curl -s -o /dev/null -w "%{http_code}\n" https://httpbingo.org/status/418
421 to 451
validation, rate limiting, locking, upgrades and legal blocks
422 Unprocessable Content
Well formed, but the values are wrong
POST /users HTTP/1.1
Content-Type: application/json
{"email": "not-an-email", "age": -3}
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://example.com/probs/validation",
"title": "Validation failed",
"status": 422,
"errors": [
{"field": "email", "detail": "must be a valid email address"},
{"field": "age", "detail": "must be 0 or more"}
]
}
429 Too Many Requests
Rate limited, with a time to wait
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/problem+json
{"title": "Rate limit exceeded", "status": 429, "detail": "100 requests per minute"}
Some limits are not 429
HTTP/1.1 403 Forbidden x-ratelimit-remaining: 0 HTTP/1.1 503 Service Unavailable Retry-After: 120
Locking, Timing and Upgrades
421, 423, 424, 425, 426 and 428
| Code | Name | When |
|---|---|---|
| 421 | Misdirected Request | an HTTP/2 connection was reused for a host this server does not serve |
| 423 | Locked | WebDAV: the resource is locked |
| 424 | Failed Dependency | WebDAV: an action failed because an earlier one did |
| 425 | Too Early | the server will not risk processing a TLS 1.3 early data request that could be replayed |
| 426 | Upgrade Required | use another protocol; the Upgrade header names it |
| 428 | Precondition Required | send If-Match or similar to prevent lost updates |
451 Unavailable for Legal Reasons
Blocked by law, with an optional pointer to who demanded it
HTTP/1.1 451 Unavailable For Legal Reasons Link: <https://authority.example/>; rel="blocked-by" Content-Type: text/html <p>This content is not available in your country.</p>
5xx Server Errors
500, 502, 503, 504 and the rest, with what usually causes each
500 Internal Server Error
Something failed that the server did not expect
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json
{"title": "Internal Server Error", "status": 500, "instance": "/orders/8812", "traceId": "4bf92f3577b34da6"}
502, 503 and 504
Three gateway errors that mean different things
| Code | Who sends it | What happened |
|---|---|---|
| 502 Bad Gateway | a proxy or load balancer | the upstream server closed the connection, sent garbage or refused it |
| 503 Service Unavailable | the server or a proxy | overloaded, in maintenance, or no healthy upstream; often temporary |
| 504 Gateway Timeout | a proxy or load balancer | the upstream server did not answer in time |
503 with Retry-After for planned downtime
HTTP/1.1 503 Service Unavailable Retry-After: 3600 Content-Type: text/html Cache-Control: no-store
Rarer 5xx Codes
501, 505 to 511
| Code | Name | When |
|---|---|---|
| 501 | Not Implemented | the server does not support the method at all, for example PROPFIND on a plain web server |
| 505 | HTTP Version Not Supported | the request used an HTTP version the server refuses |
| 506 | Variant Also Negotiates | a content negotiation configuration error (RFC 2295, experimental) |
| 507 | Insufficient Storage | WebDAV: no space to store the result |
| 508 | Loop Detected | WebDAV: infinite loop while processing a request with Depth: infinity |
| 510 | Not Extended | obsolete: the HTTP extension framework of RFC 2774 is historic |
| 511 | Network Authentication Required | a captive portal on hotel or airport Wi-Fi wants you to log in |
510 is obsolete
HTTP/1.1 510 Not Extended
Unofficial Codes
nginx, Cloudflare, AWS load balancers, IIS and other codes outside the registry
nginx
Codes nginx uses internally and in its logs
| Code | Meaning in nginx |
|---|---|
| 444 | close the connection without sending any response (return 444;) |
| 494 | request header too large, before nginx answers 400 |
| 495 | the client certificate failed verification |
| 496 | the client did not send a required certificate |
| 497 | a plain HTTP request reached the HTTPS port |
| 499 | the client closed the connection before nginx answered; only seen in logs |
Drop junk requests silently
server {
listen 80 default_server;
server_name _;
return 444;
}
Cloudflare
52x codes mean Cloudflare could not get a good answer from your origin
| Code | Cloudflare's name | Look at |
|---|---|---|
| 520 | Web server returns an unknown error | the origin sent an empty, invalid or reset response |
| 521 | Web server is down | the origin refused the connection; is it running, is the firewall blocking Cloudflare IPs? |
| 522 | Connection timed out | TCP to the origin timed out |
| 523 | Origin is unreachable | DNS or routing to the origin is broken |
| 524 | A timeout occurred | the origin accepted but did not answer within 100 seconds |
| 525 | SSL handshake failed | TLS between Cloudflare and the origin failed |
| 526 | Invalid SSL certificate | Full (strict) mode and an invalid origin certificate |
| 530 | Origin error | shown together with a 1xxx Cloudflare error |
AWS Application Load Balancer
Codes only an ALB generates
| Code | Meaning |
|---|---|
| 460 | the client closed the connection before the idle timeout |
| 463 | X-Forwarded-For held more than 30 IP addresses |
| 464 | the request protocol does not match the target group protocol version |
| 561 | the identity provider returned an error during listener authentication |
| 562 | the JWKS endpoint for JWT validation failed |
Other Codes You May Meet
Framework and vendor codes outside the registry
| Code | Source | Meaning |
|---|---|---|
| 419 | Laravel | Page Expired: the CSRF token is missing or stale |
| 420 | old Twitter API | Enhance Your Calm, an early rate limit code; use 429 |
| 440 | IIS, Exchange | Login Time-out |
| 449 | IIS | Retry With |
| 509 | cPanel, Apache modules | Bandwidth Limit Exceeded |
| 598, 599 | some proxies | network read or connect timeout |
Choosing the Right Code
decision tables for the cases people argue about
The Usual Debates
Pick by what the client can do next
| Situation | Use | Not |
|---|---|---|
| not logged in, or the token expired | 401 | 403 |
| logged in, but no permission | 403 | 401 |
| hide that the thing exists | 404 | 403 |
| JSON is broken | 400 | 422 |
| JSON is fine, the values are invalid | 422 or 400 | 500 |
| email already registered | 409 | 400 |
| stale version in If-Match | 412 | 409 |
| deleted for good | 410 | 404, though both work |
| created something | 201 with Location | 200 |
| worked, nothing to return | 204 | 200 with an empty body |
| work queued for later | 202 | 200 |
| quota exceeded | 429 | 403 |
| maintenance or overload | 503 | 500 |
Redirect Decisions
Four questions, one code
| Is the move permanent? | Must the method and body survive? | Code |
|---|---|---|
| yes | no, these are pages for GET | 301 |
| yes | yes, an API endpoint | 308 |
| no | no | 302 |
| no | yes | 307 |
| after a POST, show a result page | it becomes GET on purpose | 303 |
Error Codes for Each Layer
Who should answer with what
| Layer | Typical codes |
|---|---|
| reverse proxy or CDN | 502, 503, 504, 413, 414, 431 |
| authentication middleware | 401, 403, 429 |
| routing | 404, 405, 406, 415 |
| validation | 400, 422 |
| business logic | 409, 412, 410, 402 |
| unexpected failure | 500 |
Status Codes in REST APIs
what each HTTP method returns on success and on error
Method by Method
Success and common error codes for CRUD
| Method | Success | Common errors |
|---|---|---|
| GET /items | 200 | 400, 401, 403 |
| GET /items/7 | 200, 304 | 404, 410 |
| POST /items | 201 with Location, or 202 | 400, 409, 415, 422 |
| PUT /items/7 | 200 or 204; 201 if it created the item | 400, 404, 409, 412, 422 |
| PATCH /items/7 | 200 or 204 | 400, 404, 409, 412, 415, 422 |
| DELETE /items/7 | 204, 200 with a body, or 202 | 404, 409 |
| HEAD /items/7 | 200, 304 | 404 |
| OPTIONS /items | 204 or 200 with Allow | 405 |
DELETE twice: 204, then 404
DELETE /items/7 HTTP/1.1 HTTP/1.1 204 No Content DELETE /items/7 HTTP/1.1 HTTP/1.1 404 Not Found
Bulk and Async Work
Report a result per item
POST /items/batch HTTP/1.1
HTTP/1.1 207 Multi-Status
Content-Type: application/json
{"results": [
{"id": 1, "status": 201},
{"id": 2, "status": 422, "detail": "price must be positive"}
]}
Poll a job until it finishes
GET /jobs/9f3a HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{"status": "running", "progress": 0.4}
GET /jobs/9f3a HTTP/1.1
HTTP/1.1 303 See Other
Location: /reports/2026-10
Health Checks
What load balancers and Kubernetes expect
GET /healthz HTTP/1.1 HTTP/1.1 200 OK Content-Type: text/plain ok
Error Bodies
RFC 9457 problem details, validation errors and what to put in an error
Problem Details, RFC 9457
One JSON shape for every error
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"status": 403,
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30
}
The members
| Member | Type | Meaning |
|---|---|---|
| type | URI reference | identifies the problem type; about:blank if omitted |
| title | string | short, human readable summary of the type |
| status | number | the HTTP status code, repeated for convenience |
| detail | string | explanation of this occurrence, for humans |
| instance | URI reference | identifies this occurrence |
| anything else | any | extension members, such as errors or traceId |
Validation Errors
List every invalid field at once
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://example.com/probs/validation",
"title": "Your request is not valid.",
"status": 422,
"errors": [
{"pointer": "#/email", "detail": "must be a valid email address"},
{"pointer": "#/items/0/qty", "detail": "must be at least 1"}
]
}
Built-in Problem Details
Frameworks that emit RFC 9457 for you
// ASP.NET Core: problem details for exceptions and empty error responses builder.Services.AddProblemDetails(); app.UseExceptionHandler(); app.UseStatusCodePages();
Spring Boot
spring.mvc.problemdetails.enabled=true
What Not to Put in an Error
Leaks and dead ends
HTTP/1.1 500 Internal Server Error
{"error": "SQLSTATE[42S02]: Base table 'prod.users_v2' doesn't exist at /var/www/app/db.php:88"}
Headers That Travel With Codes
the headers a status code requires or expects
Required and Expected Headers
Code, header, and how strict the spec is
| Code | Header | Rule |
|---|---|---|
| 201 | Location | expected: the URL of the new resource |
| 3xx redirects | Location | needed, or there is nowhere to go |
| 401 | WWW-Authenticate | must: names the auth scheme |
| 405 | Allow | must: lists the supported methods |
| 407 | Proxy-Authenticate | must |
| 206 | Content-Range | must, per part |
| 416 | Content-Range | should: bytes */size |
| 429, 503 | Retry-After | should, when you know how long |
| 101, 426 | Upgrade | must: names the protocol |
| 204, 304 | (no body) | must not send content |
Retry-After
Two formats, both valid
HTTP/1.1 503 Service Unavailable Retry-After: 120 HTTP/1.1 429 Too Many Requests Retry-After: Wed, 14 Oct 2026 18:30:00 GMT
Deprecating an Endpoint
Deprecation and Sunset headers
HTTP/1.1 200 OK Deprecation: @1798761600 Sunset: Sat, 01 May 2027 00:00:00 GMT Link: <https://api.example.com/v2/orders>; rel="successor-version"
Discovery Headers
Tell clients what they can send
HTTP/1.1 415 Unsupported Media Type Accept-Post: application/json, application/merge-patch+json HTTP/1.1 405 Method Not Allowed Allow: GET, HEAD, QUERY Accept-Query: application/sql, application/jsonpath
Caching & Conditional Requests
which codes caches keep, ETag, 304 and 412
Cacheable by Default
Caches may store these without explicit Cache-Control
| Codes | Heuristically cacheable |
|---|---|
| 200, 203, 204, 206 | yes |
| 300, 301, 308 | yes |
| 404, 405, 410, 414 | yes |
| 501 | yes |
| 302, 303, 307 | only with explicit freshness, such as Cache-Control: max-age |
| anything else | only with explicit freshness |
Stop a redirect or error from being cached
HTTP/1.1 301 Moved Permanently Location: https://example.com/new Cache-Control: no-store HTTP/1.1 404 Not Found Cache-Control: max-age=60
ETag and 304
First request, then a conditional one
GET /api/config HTTP/1.1 HTTP/1.1 200 OK ETag: "a1b2c3" Cache-Control: no-cache GET /api/config HTTP/1.1 If-None-Match: "a1b2c3" HTTP/1.1 304 Not Modified ETag: "a1b2c3"
See a 304 with curl
curl -s -o /dev/null -w "%{http_code}\n" -H 'If-None-Match: "abc"' "https://httpbingo.org/etag/abc"
Conditional Writes
If-Match turns lost updates into 412
| Header | Sent with | Fails with |
|---|---|---|
| If-None-Match | GET, HEAD | 304 when the ETag matches |
| If-Modified-Since | GET, HEAD | 304 when not modified |
| If-Match | PUT, PATCH, DELETE | 412 when the ETag differs |
| If-Unmodified-Since | PUT, PATCH, DELETE | 412 when modified since |
| If-None-Match: * | PUT | 412 when the resource already exists |
Retries & Rate Limits
which codes to retry, Retry-After and backoff with jitter
Retry or Not
A retry policy by status code
| Code | Retry? | How |
|---|---|---|
| 408, 425 | yes | at once, once |
| 429 | yes | after Retry-After, or with backoff |
| 500 | maybe | only idempotent requests, with backoff |
| 502, 503, 504 | yes | with backoff; honor Retry-After on 503 |
| other 4xx | no | the request itself is wrong |
| network error, no response | yes for idempotent methods | with backoff |
Exponential Backoff With Jitter
Wait longer after each failure, with randomness
const RETRY = new Set([408, 425, 429, 500, 502, 503, 504]);
async function fetchWithRetry(url, options = {}, tries = 5) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, options);
if (!RETRY.has(res.status) || attempt === tries - 1) return res;
const header = res.headers.get("retry-after");
let wait = Math.min(30000, 500 * 2 ** attempt) * Math.random();
if (header) {
const secs = Number(header);
wait = Number.isNaN(secs) ? Date.parse(header) - Date.now() : secs * 1000;
}
await new Promise((r) => setTimeout(r, Math.max(0, wait)));
}
}
Rate Limit Headers
The common header families
HTTP/1.1 200 OK X-RateLimit-Limit: 5000 X-RateLimit-Remaining: 4987 X-RateLimit-Reset: 1792000000 HTTP/1.1 200 OK RateLimit-Policy: "default";q=100;w=60 RateLimit: "default";r=37;t=21
Redirects, 404s & SEO
how Google treats each code, soft 404s and maintenance
How Google Reads Status Codes
What each class does to indexing
| Code | Effect in Google Search |
|---|---|
| 200 | content goes to indexing; a 200 that looks like an error page is a soft 404 |
| 301, 308 | strong signal that the target is the canonical URL |
| 302, 303, 307 | weak signal; the original URL may stay indexed |
| 304 | no change; Google keeps what it has |
| 404, 410 | treated the same: dropped from the index over time |
| 429, 5xx | crawling slows down; pages that keep failing are dropped |
Soft 404s
A missing page must not answer 200
HTTP/1.1 200 OK Content-Type: text/html <h1>Sorry, we could not find that product</h1>
Sitemaps and Canonical URLs
Every URL in a sitemap should answer 200
curl -s https://example.com/sitemap.xml | grep -o "<loc>[^<]*" | cut -c6- | head -20 | xargs -I {} curl -s -o /dev/null -w "%{http_code} {}\n" {}
robots.txt Errors
Its status code changes how the whole site is crawled
| robots.txt answers | Google does |
|---|---|
| 2xx | follows the rules in the file |
| 3xx | follows at least five hops, then treats it as 404 |
| 4xx except 429 | assumes there are no crawl restrictions |
| 5xx or 429 | stops crawling the site for 12 hours, then uses a cached copy for up to 30 days |
curl & the Command Line
status codes in scripts, --fail, --write-out and redirects
Status Codes in Scripts
Fail the script on 4xx and 5xx
curl -sf https://httpbingo.org/status/503 > /dev/null; echo "exit $?"
Branch on the code in bash
code=$(curl -s -o /dev/null -w "%{http_code}" https://httpbingo.org/status/201)
case "$code" in
2??) echo "ok: $code" ;;
4??) echo "client error: $code" ;;
5??) echo "server error: $code" ;;
*) echo "no HTTP response: $code" ;;
esac
Retry transient errors automatically
curl --retry 5 --retry-max-time 60 -sf https://api.example.com/health
Redirects in curl
Where would this redirect go
curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}\n" "https://httpbingo.org/redirect-to?url=https://example.com/&status_code=308"
Count the hops and print the final URL
curl -sL -o /dev/null -w "%{num_redirects} hops, final %{http_code} at %{url_effective}\n" https://httpbingo.org/redirect/3
Many URLs at Once
Check a list of URLs in parallel
xargs -P 8 -I {} curl -s -o /dev/null -w "%{http_code} {}\n" {} < urls.txt | sort
fetch, Browsers & DevTools
response.ok, redirects, CORS and reading codes in DevTools
fetch() and Status Codes
fetch resolves on 404 and 500
const res = await fetch("https://httpbingo.org/status/404");
console.log(res.status, res.ok, res.statusText);
const r2 = await fetch("https://httpbingo.org/status/204");
console.log(r2.status, r2.ok);
Turn HTTP errors into exceptions
async function getJson(url) {
const res = await fetch(url, { headers: { Accept: "application/json" } });
if (!res.ok) {
const body = await res.text();
throw new Error(`HTTP ${res.status} for ${url}: ${body.slice(0, 200)}`);
}
return res.status === 204 ? null : res.json();
}
statusText is empty over HTTP/2 and HTTP/3
console.log(res.status, JSON.stringify(res.statusText)); // 404 ""
Redirects in fetch
Browsers hide manual redirects, Node.js does not
const res = await fetch("https://httpbingo.org/redirect-to?url=/get&status_code=302", { redirect: "manual" });
console.log(res.status, res.type, res.headers.get("location"));
const followed = await fetch("https://httpbingo.org/redirect/2");
console.log(followed.status, followed.redirected, followed.url);
CORS and Status 0
A CORS failure has no status at all
fetch("https://other-site.example/api")
.then((res) => console.log(res.status))
.catch((err) => console.log(err.name, err.message));
// TypeError Failed to fetch
DevTools Network Tab
Reading codes in the browser
| Shown | Meaning |
|---|---|
| 200 (from disk cache) | no request was sent; the cached copy was used |
| 200 (from memory cache) | same, from this page's memory cache |
| 304 | revalidated with the server, body from cache |
| (canceled) or (failed) | no status: aborted, blocked or a network error |
| CORS error | the response arrived but JavaScript may not read it |
| 307 Internal Redirect | Chrome upgrading http to https by HSTS, not a server response |
Setting Codes in Code
Express, Python, Spring, Go, PHP, .NET and Rails side by side
Created, With a Location
Express
app.post("/users", (req, res) => {
const user = createUser(req.body);
res.status(201).location(`/users/${user.id}`).json(user);
});
Next.js App Router route handler
export async function POST(request) {
const user = await createUser(await request.json());
return Response.json(user, { status: 201, headers: { Location: `/api/users/${user.id}` } });
}
Flask and FastAPI
# Flask: return body, status, headers
@app.post("/users")
def create_user():
user = save(request.get_json())
return user, 201, {"Location": f"/users/{user['id']}"}
# FastAPI: declare the success code on the route
@app.post("/users", status_code=status.HTTP_201_CREATED)
def create_user(user: UserIn, response: Response):
saved = save(user)
response.headers["Location"] = f"/users/{saved.id}"
return saved
Spring Boot
@PostMapping("/users")
ResponseEntity<User> create(@RequestBody User user) {
User saved = repository.save(user);
return ResponseEntity.created(URI.create("/users/" + saved.getId())).body(saved);
}
Go 1.22 and later
mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
user := createUser(r)
w.Header().Set("Location", fmt.Sprintf("/users/%d", user.ID))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
})
PHP and Laravel
// plain PHP
http_response_code(201);
header("Location: /users/{$user['id']}");
header('Content-Type: application/json');
echo json_encode($user);
// Laravel
return response()->json($user, 201)->header('Location', "/users/{$user->id}");
ASP.NET Core minimal API
app.MapPost("/users", (UserIn input, AppDb db) =>
{
var user = db.Add(input);
return TypedResults.Created($"/users/{user.Id}", user);
});
Ruby on Rails
def create @user = User.create!(user_params) render json: @user, status: :created, location: @user end
Not Found and Other Errors
Express
app.get("/users/:id", (req, res) => {
const user = findUser(req.params.id);
if (!user) return res.status(404).json({ title: "User not found", status: 404 });
res.json(user);
});
app.delete("/users/:id", (req, res) => { removeUser(req.params.id); res.sendStatus(204); });
Flask, FastAPI and Django
abort(404) # Flask
raise HTTPException(status_code=404, detail="User not found") # FastAPI
raise Http404("User not found") # Django
return JsonResponse({"error": "conflict"}, status=409) # Django
Spring Boot
@GetMapping("/users/{id}")
User get(@PathVariable long id) {
return repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found"));
}
@DeleteMapping("/users/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void delete(@PathVariable long id) { repository.deleteById(id); }
Go
http.Error(w, "user not found", http.StatusNotFound)
PHP and Laravel
http_response_code(404); // plain PHP abort(404); // Laravel abort(403, 'Not allowed'); return response()->noContent(); // 204
ASP.NET Core
app.MapGet("/users/{id}", (int id, AppDb db) =>
db.Find(id) is { } user ? Results.Ok(user) : Results.NotFound());
app.MapGet("/status", () => Results.Problem(statusCode: 503, title: "Down for maintenance"));
Rails
head :no_content
render json: { error: "conflict" }, status: :conflict
raise ActiveRecord::RecordNotFound # rendered as 404 in production
Redirects and Their Default Codes
Express and Next.js
res.redirect("/login"); // 302 by default
res.redirect(301, "/new-page");
// Next.js: redirect() sends 307, permanentRedirect() sends 308
Flask, FastAPI and Django
return redirect("/new-page", code=301) # Flask, default 302
return RedirectResponse("/new-page", status_code=301) # FastAPI, default 307
return redirect("/new-page", permanent=True) # Django, default 302
Spring
return "redirect:/orders/" + id; // 302 from a @Controller
return ResponseEntity.status(HttpStatus.SEE_OTHER).location(URI.create("/orders/" + id)).build();
Go
http.Redirect(w, r, "/new-page", http.StatusMovedPermanently)
PHP and Laravel
header('Location: /new-page', true, 301); // without a code PHP sends 302
exit;
return redirect('/new-page', 301); // Laravel, default 302
ASP.NET Core
Results.Redirect("/new-page"); // 302
Results.Redirect("/new-page", permanent: true); // 301
Results.Redirect("/new-page", permanent: true, preserveMethod: true); // 308
Rails
redirect_to "/new-page" # 302 redirect_to "/new-page", status: :moved_permanently # 301 redirect_to order_path(@order), status: :see_other # 303, after a form
Codes Without a Framework
Node.js http sends the reason phrase for you
const http = require("http");
const server = http.createServer((req, res) => {
res.writeHead(418, { "Content-Type": "text/plain" });
res.end("short and stout");
});
server.listen(0, async () => {
const res = await fetch(`http://localhost:${server.address().port}/`);
console.log(res.status, res.statusText, await res.text());
server.close();
});
Python urllib raises on 4xx and 5xx
import urllib.request, urllib.error
try:
urllib.request.urlopen("https://httpbingo.org/status/404")
except urllib.error.HTTPError as e:
print(e.code, e.reason)
from http import HTTPStatus
s = HTTPStatus(429)
print(s.value, s.phrase, s.is_client_error, s.is_server_error)
Testing Status Codes
asserting codes in unit, integration and load tests
Integration Tests
node:test with fetch, no extra packages
const test = require("node:test");
const assert = require("node:assert");
const http = require("http");
const app = http.createServer((req, res) => {
if (req.method === "POST" && req.url === "/items") {
res.writeHead(201, { Location: "/items/42" });
return res.end();
}
res.writeHead(404).end();
});
test("POST /items answers 201 with a Location", async (t) => {
await new Promise((resolve) => app.listen(0, resolve));
t.after(() => app.close());
const base = `http://localhost:${app.address().port}`;
const res = await fetch(`${base}/items`, { method: "POST" });
assert.strictEqual(res.status, 201);
assert.strictEqual(res.headers.get("location"), "/items/42");
assert.strictEqual((await fetch(`${base}/nope`)).status, 404);
});
pytest with the Flask or FastAPI test client
def test_create_item(client):
r = client.post("/items", json={"name": "lamp"})
assert r.status_code == 201
assert r.headers["Location"] == "/items/42"
def test_missing_item(client):
assert client.get("/items/999").status_code == 404
Spring MockMvc
mockMvc.perform(post("/items")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"lamp\"}"))
.andExpect(status().isCreated())
.andExpect(header().string("Location", "/items/42"));
mockMvc.perform(get("/items/999")).andExpect(status().isNotFound());
Playwright API checks
test("create and read an item", async ({ request }) => {
const created = await request.post("/api/items", { data: { name: "lamp" } });
expect(created.status()).toBe(201);
const item = await request.get(created.headers()["location"]);
await expect(item).toBeOK();
});
Python requests
Check the code, then raise for errors
import requests
r = requests.get("https://httpbingo.org/status/503", timeout=10)
print(r.status_code, r.ok, r.reason)
try:
r.raise_for_status()
except requests.HTTPError as e:
print("HTTPError:", e)
Postman and Load Tests
Postman test script
pm.test("created", () => {
pm.response.to.have.status(201);
pm.response.to.have.header("Location");
});
k6 load test with a status check
import http from "k6/http";
import { check } from "k6";
export default function () {
const res = http.get("https://test.k6.io/");
check(res, { "status is 200": (r) => r.status === 200 });
}
nginx, Apache & Caddy
redirects, error pages, size limits and rate limits in the web server
nginx
Redirects
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
location = /old-page { return 301 /new-page; }
rewrite ^/blog/(.*)$ /articles/$1 permanent; # 301; redirect flag gives 302
Error pages and changed codes
error_page 404 /errors/404.html;
error_page 502 503 504 /errors/50x.html;
location /errors/ { internal; }
error_page 404 =200 /empty.gif; # answer 200 instead
proxy_intercept_errors on; # also use them for upstream errors
Size limits, rate limits and timeouts
client_max_body_size 50m; # default 1m, beyond it 413
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429; # default 503
proxy_read_timeout 120s; # default 60s, then 504
proxy_pass http://app:8080;
}
Apache
Redirects in .htaccess
Redirect 301 /old-page /new-page
RedirectMatch 301 ^/blog/(.*)$ /articles/$1
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Error documents
ErrorDocument 404 /errors/404.html ErrorDocument 500 "Something went wrong. Please try again later."
Caddy
Redirects, errors and fixed responses
example.com {
redir /old-page /new-page 301
respond /healthz 200
handle_errors {
rewrite * /errors/{err.status_code}.html
file_server
}
reverse_proxy app:8080
}
Proxies, Load Balancers & CDNs
where 502, 503 and 504 really come from
Who Sent This Error
Read the response headers, not just the code
HTTP/1.1 502 Bad Gateway Server: cloudflare CF-RAY: 8d2c1f0e9a7b1234-LHR HTTP/1.1 504 Gateway Time-out Server: nginx/1.28.0 Content-Type: text/html <html><body><center><h1>504 Gateway Time-out</h1></center><hr><center>nginx/1.28.0</center></body></html>
nginx Upstream Errors
Match the error log line to the fix
| error.log says | Client gets | Usual fix |
|---|---|---|
| connect() failed (111: Connection refused) | 502 | the app is down or listens on another port or address |
| no live upstreams | 502 | every upstream server failed its checks |
| upstream prematurely closed connection | 502 | the app crashed mid-response or closed a keep-alive connection |
| upstream sent too big header | 502 | raise proxy_buffer_size for big cookies or headers |
| upstream timed out (110) while reading response header | 504 | the app is slow; speed it up or raise proxy_read_timeout |
| client intended to send too large body | 413 | raise client_max_body_size |
499 and 504 Together
Two views of the same slow request
# nginx access log 10.0.0.5 - - [14/Oct/2026:18:22:41 +0000] "GET /report HTTP/1.1" 499 0 "-" "curl/8.19.0"
HTTP/2, HTTP/3 & Other Protocols
:status, gRPC, GraphQL and WebSocket codes
HTTP/2 and HTTP/3
The code arrives as the :status pseudo-header
const http2 = require("http2");
const client = http2.connect("https://github.com");
const req = client.request({ ":path": "/this-page-does-not-exist" });
req.on("response", (headers) => {
console.log(headers[":status"], headers["content-type"]);
client.close();
});
req.end();
curl prints the protocol and no reason phrase
$ curl -sI https://github.com | head -1 HTTP/2 200
gRPC Status Codes
gRPC answers HTTP 200 and puts its own code in a trailer
| gRPC code | Number | HTTP equivalent |
|---|---|---|
| OK | 0 | 200 |
| CANCELLED | 1 | 499 |
| INVALID_ARGUMENT | 3 | 400 |
| DEADLINE_EXCEEDED | 4 | 504 |
| NOT_FOUND | 5 | 404 |
| ALREADY_EXISTS | 6 | 409 |
| PERMISSION_DENIED | 7 | 403 |
| RESOURCE_EXHAUSTED | 8 | 429 |
| FAILED_PRECONDITION | 9 | 400 |
| ABORTED | 10 | 409 |
| UNIMPLEMENTED | 12 | 501 |
| INTERNAL | 13 | 500 |
| UNAVAILABLE | 14 | 503 |
| UNAUTHENTICATED | 16 | 401 |
GraphQL and WebSockets
GraphQL errors often travel in a 200
HTTP/1.1 200 OK
Content-Type: application/json
{"data": {"user": null}, "errors": [{"message": "User not found", "path": ["user"]}]}
WebSocket close codes are not HTTP codes
| Close code | Meaning |
|---|---|
| 1000 | normal closure |
| 1001 | going away: page closed or server restarting |
| 1006 | abnormal closure, no close frame (set by the client, never sent) |
| 1008 | policy violation |
| 1009 | message too big |
| 1011 | internal server error |
Monitoring & Logs
counting codes in logs, 5xx rates, alerts and error budgets
Count Codes in an Access Log
Which codes, how often
awk '{print $9}' access.log | sort | uniq -c | sort -rn | head -8
The share of server errors
awk '{n++} $9 ~ /^5/ {e++} END {printf "%d of %d requests were 5xx (%.1f%%)\n", e, n, 100 * e / n}' access.log
The URLs behind the errors
awk '$9 >= 500 {print $9, $7}' access.log | sort | uniq -c | sort -rn
Prometheus and Alerts
5xx ratio over five minutes
sum(rate(http_requests_total{code=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
An alerting rule
groups:
- name: http
rules:
- alert: HighErrorRate
expr: sum(rate(http_requests_total{code=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.02
for: 10m
labels:
severity: page
annotations:
summary: "More than 2% of requests fail with 5xx"
Error Budgets
What 99.9% allows
| Availability target | Failed requests allowed | Full downtime per 30 days |
|---|---|---|
| 99% | 1 in 100 | 7.2 hours |
| 99.9% | 1 in 1,000 | 43.2 minutes |
| 99.95% | 1 in 2,000 | 21.6 minutes |
| 99.99% | 1 in 10,000 | 4.3 minutes |
Troubleshooting by Code
a checklist for the errors that cost the most time
Client Errors
First checks for 4xx
| Code | Check first |
|---|---|
| 400 | malformed JSON, wrong Content-Type, oversized cookies, a typo in a query parameter |
| 401 | missing or expired token, wrong auth scheme, clock skew breaking JWT validation |
| 403 | file permissions on static files, a WAF rule, CORS preflight blocked, wrong role |
| 404 | trailing slash, case sensitivity, SPA routes without a server fallback, wrong base path behind a proxy |
| 405 | form posting to a static file server, a missing route for that method |
| 413 | client_max_body_size in nginx, upload limits in PHP (upload_max_filesize) or the framework |
| 415 | Content-Type header missing or not application/json |
| 429 | client loops without backoff, a shared IP behind NAT hitting a per-IP limit |
Server Errors
First checks for 5xx
| Code | Check first |
|---|---|
| 500 | the application log and the stack trace for that request ID |
| 502 | is the app running and listening where the proxy expects; proxy error log |
| 503 | maintenance mode, no healthy targets, connection pool or queue exhausted |
| 504 | slow queries, slow third-party calls, proxy timeouts shorter than the work |
Specs & History
the RFCs that define status codes, from 1996 to 2026
Where the Codes Are Defined
The documents that matter
| Year | Document | Status codes |
|---|---|---|
| 1996 | RFC 1945, HTTP/1.0 | the first 15: 200, 201, 202, 204, 301, 302, 304, 400, 401, 403, 404, 500, 501, 502, 503 |
| 1997 and 1999 | RFC 2068 and RFC 2616, HTTP/1.1 | most of today's list, including 100, 101, 307 and 409 to 417 |
| 2012 | RFC 6585 | 428, 429, 431, 511 |
| 2015 | RFC 7538 | 308 Permanent Redirect |
| 2016 | RFC 7725 | 451 Unavailable For Legal Reasons |
| 2017 | RFC 8297 | 103 Early Hints |
| 2018 | RFC 8470 | 425 Too Early |
| 2022 | RFC 9110, HTTP Semantics | the current definition of the core codes; adds 421 and 422 to the core |
| 2026 | RFC 10008 | the QUERY method; no new codes, but it defines how QUERY uses 303 and Location |
Renamed and Retired
Names changed in RFC 9110 and before
| Code | Old name | Current name |
|---|---|---|
| 413 | Request Entity Too Large, then Payload Too Large | Content Too Large |
| 414 | Request-URI Too Long | URI Too Long |
| 416 | Requested Range Not Satisfiable | Range Not Satisfiable |
| 422 | Unprocessable Entity | Unprocessable Content |
| 418 | I'm a teapot (RFC 2324) | (Unused), reserved |
| 306 | Switch Proxy (draft) | (Unused) |
Every Registered Code
the full IANA registry in one table
The IANA Registry, October 2026
All registered codes, with the document that defines each
| Code | Name | Defined in | Cacheable by default |
|---|---|---|---|
| 100 | Continue | RFC 9110, Section 15.2.1 | |
| 101 | Switching Protocols | RFC 9110, Section 15.2.2 | |
| 102 | Processing | RFC 2518 | |
| 103 | Early Hints | RFC 8297 | |
| 104 | Upload Resumption Supported, temporary until 2026-11-13 | draft-ietf-httpbis-resumable-upload-05 | |
| 200 | OK | RFC 9110, Section 15.3.1 | yes |
| 201 | Created | RFC 9110, Section 15.3.2 | |
| 202 | Accepted | RFC 9110, Section 15.3.3 | |
| 203 | Non-Authoritative Information | RFC 9110, Section 15.3.4 | yes |
| 204 | No Content | RFC 9110, Section 15.3.5 | yes |
| 205 | Reset Content | RFC 9110, Section 15.3.6 | |
| 206 | Partial Content | RFC 9110, Section 15.3.7 | yes |
| 207 | Multi-Status | RFC 4918 | |
| 208 | Already Reported | RFC 5842 | |
| 226 | IM Used | RFC 3229 | |
| 300 | Multiple Choices | RFC 9110, Section 15.4.1 | yes |
| 301 | Moved Permanently | RFC 9110, Section 15.4.2 | yes |
| 302 | Found | RFC 9110, Section 15.4.3 | |
| 303 | See Other | RFC 9110, Section 15.4.4 | |
| 304 | Not Modified | RFC 9110, Section 15.4.5 | |
| 305 | Use Proxy | RFC 9110, Section 15.4.6 | |
| 306 | (Unused) | RFC 9110, Section 15.4.7 | |
| 307 | Temporary Redirect | RFC 9110, Section 15.4.8 | |
| 308 | Permanent Redirect | RFC 9110, Section 15.4.9 | yes |
| 400 | Bad Request | RFC 9110, Section 15.5.1 | |
| 401 | Unauthorized | RFC 9110, Section 15.5.2 | |
| 402 | Payment Required | RFC 9110, Section 15.5.3 | |
| 403 | Forbidden | RFC 9110, Section 15.5.4 | |
| 404 | Not Found | RFC 9110, Section 15.5.5 | yes |
| 405 | Method Not Allowed | RFC 9110, Section 15.5.6 | yes |
| 406 | Not Acceptable | RFC 9110, Section 15.5.7 | |
| 407 | Proxy Authentication Required | RFC 9110, Section 15.5.8 | |
| 408 | Request Timeout | RFC 9110, Section 15.5.9 | |
| 409 | Conflict | RFC 9110, Section 15.5.10 | |
| 410 | Gone | RFC 9110, Section 15.5.11 | yes |
| 411 | Length Required | RFC 9110, Section 15.5.12 | |
| 412 | Precondition Failed | RFC 9110, Section 15.5.13 | |
| 413 | Content Too Large | RFC 9110, Section 15.5.14 | |
| 414 | URI Too Long | RFC 9110, Section 15.5.15 | yes |
| 415 | Unsupported Media Type | RFC 9110, Section 15.5.16 | |
| 416 | Range Not Satisfiable | RFC 9110, Section 15.5.17 | |
| 417 | Expectation Failed | RFC 9110, Section 15.5.18 | |
| 418 | (Unused) | RFC 9110, Section 15.5.19 | |
| 421 | Misdirected Request | RFC 9110, Section 15.5.20 | |
| 422 | Unprocessable Content | RFC 9110, Section 15.5.21 | |
| 423 | Locked | RFC 4918 | |
| 424 | Failed Dependency | RFC 4918 | |
| 425 | Too Early | RFC 8470 | |
| 426 | Upgrade Required | RFC 9110, Section 15.5.22 | |
| 428 | Precondition Required | RFC 6585 | |
| 429 | Too Many Requests | RFC 6585 | |
| 431 | Request Header Fields Too Large | RFC 6585 | |
| 451 | Unavailable For Legal Reasons | RFC 7725 | |
| 500 | Internal Server Error | RFC 9110, Section 15.6.1 | |
| 501 | Not Implemented | RFC 9110, Section 15.6.2 | yes |
| 502 | Bad Gateway | RFC 9110, Section 15.6.3 | |
| 503 | Service Unavailable | RFC 9110, Section 15.6.4 | |
| 504 | Gateway Timeout | RFC 9110, Section 15.6.5 | |
| 505 | HTTP Version Not Supported | RFC 9110, Section 15.6.6 | |
| 506 | Variant Also Negotiates | RFC 2295 | |
| 507 | Insufficient Storage | RFC 4918 | |
| 508 | Loop Detected | RFC 5842 | |
| 510 | Not Extended (OBSOLETED) | RFC 2774, Status change of HTTP experiments to Historic | |
| 511 | Network Authentication Required | RFC 6585 |
What Is an HTTP Status Codes Cheat Sheet
An HTTP status codes cheat sheet is a compact reference to the three-digit codes servers send back with every response, arranged so you can see in seconds what a code means, which headers belong with it and what the client should do next. People also search for a list of HTTP status codes or HTTP response codes; they usually need one at two moments: while designing an API, and while staring at a 502 at two in the morning.
This cheat sheet covers 28 sections and 156 snippets: every code in the IANA registry, the unofficial codes that nginx, Cloudflare and AWS load balancers add, and the practical side of using them, from choosing between 401 and 403 to setting a 201 in eleven frameworks. Rows about vendor codes carry a chip, and the standard codes only button hides them. The output under the curl, Node.js and Python examples is what they printed when run on October 14, 2026.
The toolbox at the top checks a raw response or the output of curl -i and curl -iL against the rules of RFC 9110, follows redirect chains, looks up any code by number, range or name, suggests a code for a situation described in plain words, and tells you whether a client should retry. The builders write RFC 9457 error bodies, redirect rules for nine servers and platforms, status code handlers for eleven frameworks and custom error page configuration. Everything runs in your browser.
How It Differs From the RFCs
RFC 9110 defines the semantics precisely; this page puts each code next to the mistake that usually comes with it: the 200 with an error in the body, the 401 without WWW-Authenticate, the permanent redirect cached by every browser during testing, the 500 that leaks a stack trace. For designing the APIs that send these codes, see the REST API cheat sheet; for the data format most API errors use, the JSON cheat sheet; for scripting the curl checks, the bash cheat sheet.
What This HTTP Status Codes Cheat Sheet Covers
The 28 sections go from reading a status line to debugging a gateway timeout, and each link jumps to that section above.
- Start: the toolbox, builders and the status line, and the five classes and the codes you see most.
- The codes: 1xx informational, 2xx success, 3xx redirects, the client errors 400 to 409, 410 to 418 and 421 to 451, 5xx server errors, and unofficial codes.
- Using them: choosing the right code, REST API conventions, error bodies, headers, caching and 304, retries and rate limits, and SEO.
- Tools: curl, fetch and DevTools, frameworks, testing, nginx, Apache and Caddy, proxies and CDNs, HTTP/2, gRPC and GraphQL, and monitoring and logs.
- Help: troubleshooting by code, specs and history, and the full registry.
HTTP Status Codes Quick Reference
The decisions people look up most often, one line each. The sections above explain every one of them with a full example.
| Task | Code |
|---|---|
| Return data | 200 OK |
| Created a resource | 201 Created plus Location |
| Queued for later | 202 Accepted |
| Success, no body | 204 No Content |
| Moved for good | 301, or 308 to keep the method |
| Moved for now | 302, or 307 to keep the method |
| Result page after a form | 303 See Other |
| Cached copy is fine | 304 Not Modified |
| Malformed request | 400 Bad Request |
| Not logged in | 401 plus WWW-Authenticate |
| Logged in, not allowed | 403 Forbidden |
| Nothing here | 404 Not Found |
| Wrong method | 405 plus Allow |
| Duplicate or state clash | 409 Conflict |
| Stale version | 412 Precondition Failed |
| Invalid values | 422 Unprocessable Content |
| Rate limited | 429 plus Retry-After |
| Unexpected crash | 500 Internal Server Error |
| Maintenance | 503 plus Retry-After |
| Error body format | application/problem+json |
What an HTTP Status Code Is
Every HTTP response starts with a status code, a three-digit integer from 100 to 599 that tells the client how the server handled the request. HTTP/1.1 sends it on the first line together with a reason phrase; HTTP/2 and HTTP/3 send it as the :status pseudo-header and drop the phrase. Software acts on the number alone.
The first digit sets the class, and the class is the part every client must understand: an unknown 4xx code is handled like 400, an unknown 5xx like 500. That rule is why new codes such as 103 Early Hints or 425 Too Early could be added without breaking old software. The current definitions come from RFC 9110, published in June 2022, and IANA keeps the official registry, which this page reproduces in full.
Status codes matter most in places where software reads them without a person looking: caches decide what to store, search engines decide what to index, retry logic decides whether to try again, and monitoring decides whether to wake someone up. A wrong code misleads all four at once, which is why back-end development teams treat them as part of the API contract.
Where They Show Up
You meet status codes in browser DevTools, in curl output, in web server and load balancer logs, in API client libraries and in API integration work, where a partner's 429 or 503 decides how your own code behaves. They also shape communication between services in a microservices system, where one wrong 500 can trigger retries across a whole chain of calls.
How a Request Gets Its Status Code
A response often passes through several layers, and any of them can set the code you finally see.
- The client sends the request, possibly through a CDN such as Cloudflare and a load balancer, each of which can answer on its own with 4xx limits or 5xx gateway errors.
- A reverse proxy such as nginx applies its own rules: body size limits (413), rate limits (503 or 429), redirects and timeouts (504).
- The application's router picks a handler or answers 404 or 405 itself.
- Middleware checks authentication (401), permissions (403) and the request format (400, 415), and validation returns 422 or 400.
- The handler runs and returns a success code, a business error such as 409, or throws, which the framework turns into 500.
- On the way back, proxies may replace error pages, add caching headers or turn a broken upstream connection into 502.
The layer that produced a response leaves traces in headers such as Server, Via and CF-RAY, which is the first thing to check when a code makes no sense.
Status Codes Developers Confuse
The pairs that cause the most arguments in code reviews, and the question that settles each.
| Pair | Ask | Answer |
|---|---|---|
| 401 or 403 | Would logging in help? | yes: 401, no: 403 |
| 400 or 422 | Could the server parse it? | no: 400, yes: 422 |
| 404 or 410 | Was it removed on purpose, for good? | yes: 410, unknown: 404 |
| 409 or 412 | Did the client send If-Match? | yes: 412, no: 409 |
| 301 or 308 | Must POST stay POST? | yes: 308, no: 301 |
| 500 or 503 | Is it temporary and expected? | yes: 503, no: 500 |
| 502 or 504 | Did the upstream answer at all? | badly: 502, too late: 504 |
| 200 or 204 | Is there a body? | yes: 200, no: 204 |
Common Status Code Mistakes
The same handful of problems shows up in most API reviews and incident reports.
In APIs
- Returning 200 with an error message in the body, which hides failures from caches, clients and monitoring.
- Sending 401 without a WWW-Authenticate header, or using 401 when the user is logged in but lacks permission.
- Using 500 for invalid input, which sends people hunting for a server bug that is really a client mistake.
- Forgetting Location on 201 and on redirects, and Retry-After on 429 and 503.
- Leaking stack traces and SQL errors in 500 responses.
On Websites
- Testing with 301 and getting a redirect stuck in your own browser cache; test with 302 or 307 first.
- Serving a not found page with 200, which search engines report as a soft 404.
- Chaining redirects, such as http to www to https to the final page, instead of one hop.
- Showing a maintenance page with 200 instead of 503, which can replace your content in search results.
How to Use Status Codes Well
Design Them With the API
Write down which codes each endpoint returns before writing the handlers, and keep one error body format, RFC 9457 problem details, across every service. Test the error paths as carefully as the happy path: API and integration tests that assert the exact status code catch most of the mistakes above before they ship.
Then Watch Them in Production
Graph 4xx and 5xx rates separately and per endpoint: a jump in 5xx after a release is the clearest signal that the deployment broke something, and a good reason for a rollback. A jump in 4xx often means a client release or a documentation problem instead. Debug individual failures with the curl commands above and the browser tools described in the guide to debugging JavaScript.
How This HTTP Status Codes Cheat Sheet Is Maintained
This page is written and maintained by Bogdan Sandu for TMS Outsource, a software development company. It was checked on October 14, 2026 against RFC 9110, the IANA HTTP Status Code Registry, RFC 9457 and RFC 10008, and the vendor documentation of nginx, Cloudflare and AWS. The output of the examples was produced by running them with curl 8.19, Node.js 24 and Python 3.14. If a snippet is wrong or out of date, the date in the byline shows when it was last reviewed.
HTTP Status Code Questions People Actually Ask
The questions developers search for most about HTTP status codes, answered in a few sentences each.
What are HTTP status codes?
HTTP status codes are three-digit numbers a server sends at the start of every response to say how the request went. The first digit is the class: 1xx informational, 2xx success, 3xx redirection, 4xx client error and 5xx server error. RFC 9110 defines the core codes and IANA keeps the official registry.
What is the difference between 401 and 403?
401 Unauthorized means the request is not authenticated: credentials are missing, wrong or expired, and the response must include a WWW-Authenticate header. 403 Forbidden means the server knows who you are and still refuses; logging in again will not help. A server can also answer 404 to hide that a resource exists.
Should I use 400 or 422 for validation errors?
Use 400 Bad Request when the request cannot be parsed, such as broken JSON or a missing required header, and 422 Unprocessable Content when it parses but the values fail validation or business rules. Both are valid HTTP; pick one convention for your API and document it.
What is the difference between 301, 302, 307 and 308?
301 and 308 are permanent redirects, 302 and 307 temporary ones. With 301 and 302 clients may change a POST into a GET when following the redirect; 307 and 308 require the same method and body. For pages, 301 and 302 are fine; for APIs that receive POST or PUT, use 308 or 307.
What does 429 Too Many Requests mean?
429 means the client sent too many requests in a given time and hit a rate limit. The response should include Retry-After with the number of seconds to wait or a date. Clients should wait at least that long and then retry with exponential backoff and jitter.
What causes 502 Bad Gateway and 504 Gateway Timeout?
Both come from a proxy, load balancer or CDN in front of the application. 502 means the upstream server refused the connection, closed it early or sent an invalid response, often because the app is down or listens on another port. 504 means the upstream did not answer in time, usually a slow request or a proxy timeout shorter than the work.
Is 418 I'm a teapot a real status code?
418 comes from RFC 2324, an April Fools' joke from 1998 about coffee pots. It was never a real HTTP status code, but so much software implements it that RFC 9110 marks it as unused and reserved so that nobody assigns 418 to something else.
Which status code should an API return after creating a resource?
201 Created, with a Location header pointing to the new resource and usually the created resource in the body. If the work is queued and not finished yet, return 202 Accepted with a URL the client can poll.
Is this HTTP status codes cheat sheet up to date?
Yes. It was checked on October 14, 2026 against RFC 9110, the IANA HTTP Status Code Registry, RFC 9457 for problem details and RFC 10008, which added the QUERY method in June 2026. The command and code examples that show output were run with curl 8.19, Node.js 24 and Python 3.14.
Can I download this HTTP status codes cheat sheet as a PDF?
Yes. The page has a print stylesheet, so your browser's print command with Save as PDF produces a clean copy with every output and explanation expanded and the navigation left out. The print core button prints only the core snippets for a short reference.