Five classes and the history behind them
The first digit of the code reveals the category, and that reading alone tells you which side to look at. It is the triage a good developer does in half a second before opening any log. The meaning of each class is fixed in RFC 9110 (HTTP Semantics), the document that today serves as the reference for HTTP/1.1, HTTP/2 and HTTP/3 at once.
| Class | Meaning | Whose problem |
|---|---|---|
| 1xx | Informational: a provisional response, the request continues. | Nobody’s, it is an intermediate signal. |
| 2xx | Success: the request was received, understood and accepted. | Nobody’s, it worked. |
| 3xx | Redirection: further action is needed (go to another URL, use the cache). | Shared: follow the instruction. |
| 4xx | Client error: syntax, authentication, permission or resource. | Your request, fix what you sent. |
| 5xx | Server error: it failed to fulfil an apparently valid request. | The server, the other side fell over. |
That split looks obvious today, but it took decades to settle. The numbering was born lean and gained codes as the web discovered new problems, from Wi-Fi portals to rate limiting. It is worth knowing the timeline, because it explains why near-identical pairs like 302 and 307 exist: they are from different eras, created to fix ambiguities from the previous generation.
- May 1996RFC 1945, HTTP/1.0
The first documented version. Introduces the status line and a handful of codes (200, 301, 302, 400, 404, 500).
- January 1997RFC 2068, HTTP/1.1
The first standardized cut of HTTP/1.1, with persistent connections and more codes.
- June 1999RFC 2616, HTTP/1.1 revised
The revision that stood as the reference for fifteen years, in a single monolithic document.
- June 2014RFC 7230–7235
RFC 2616 is split into six documents (syntax, semantics, conditionals, range, caching, authentication).
- June 2022RFC 9110 / 9111 / 9112
The current reference: semantics (9110), caching (9111) and HTTP/1.1 (9112). Obsoletes the 723x series and applies to every version.
Redirecting without breaking the method: 301 vs 308, 302 vs 307
The 2xx block is the calm one: 200 OK is success with a body, 201 Created confirms a POST created a resource (usually with a Location header) and 204 No Content is the silent success, typical of a DELETE. The real trap is in 3xx, where one subtle decision separates a harmless redirect from a bug that only shows up in production: does the code change the HTTP method?
Here is the detail RFC 9110 records in a note: for historical reasons, when following a 301 (§15.4.2) or a 302 (§15.4.3) the client MAY change the method from POST to GET. Browsers have done this for decades, and the spec merely describes the real behavior. The 307 Temporary Redirect (§15.4.8) and 308 Permanent Redirect (§15.4.9) were created precisely to NOT allow that change: they preserve the method and body. The permanent/temporary axis is orthogonal to this, 301 and 308 are permanent; 302 and 307 are temporary.
| Code | Duration | Method preserved? | Cacheable by default? | What the crawler does |
|---|---|---|---|---|
| 301 Moved Permanently | Permanent | No, may turn POST→GET | Yes | Consolidates signals to the new URL |
| 308 Permanent Redirect | Permanent | Yes, keeps method and body | Yes | Consolidates signals to the new URL |
| 302 Found | Temporary | No, may turn POST→GET | No | Keeps the original URL indexed |
| 307 Temporary Redirect | Temporary | Yes, keeps method and body | No | Keeps the original URL indexed |
Worked example #1: a checkout sends a POST with the purchase amount. If the server responds 302, the browser follows the redirect by switching the method to GET and DROPPING the body, the payment is lost on the way and the confirmation endpoint gets an empty GET. With 307, the client repeats the POST with the same body. See the difference byte by byte:
# Envio um formulário de pagamento com POST
POST /checkout HTTP/1.1
Host: loja.com
Content-Type: application/x-www-form-urlencoded
valor=199.90&metodo=cartao
# (A) Servidor responde 302 -> o navegador troca POST por GET
HTTP/1.1 302 Found
Location: /obrigado
GET /obrigado HTTP/1.1 <- metodo virou GET, corpo descartado
Host: loja.com
# (B) Mesmo fluxo com 307 -> o cliente REPETE metodo e corpo
HTTP/1.1 307 Temporary Redirect
Location: /obrigado
POST /obrigado HTTP/1.1 <- continua POST, corpo preservado
Host: loja.com
Content-Type: application/x-www-form-urlencoded
valor=199.90&metodo=cartaoThe rule of thumb falls out on its own: to permanently move a plain page (GET) and preserve ranking, 301 is the classic and Google treats 301 and 308 as equivalent when consolidating signals. To permanently move an endpoint that receives POST, use 308. For a passing detour, 302 is enough, unless the method matters, and then it is 307. Check the meaning, RFC and cacheability of any code in the tool below before you choose.
4xx: the request is yours, 401 vs 403, 404 vs 410
The 4xx points at what you sent. Some are blunt: 400 Bad Request is a malformed request, 405 Method Not Allowed is the wrong verb (and the server should list the accepted ones in the Allow header), 409 Conflict is a state clash. But two pairs make people waste hours hunting a bug in the wrong place: 401 versus 403 and 404 versus 410. The difference is not one of severity, it is one of meaning.
401 Unauthorized
- “I don’t know who you are”: valid authentication is missing (token absent, expired or invalid).
- Re-authenticating, logging in again, refreshing the token, may fix it.
- RFC 9110 §15.5.2 REQUIRES the WWW-Authenticate header with a challenge.
403 Forbidden
- “I know who you are and you can’t”: permission is missing (role, scope, IP, WAF rule).
- Re-authenticating does not help, the problem is authorization, not identity.
- The server may explain why or, to hide the resource’s existence, answer 404 instead.
The other pair lives on the border between backend and SEO. 404 Not Found (§15.5.5) says “does not exist at this URL”, it could be a wrong route, a removed resource, or even the server hiding that the resource exists. 410 Gone (§15.5.11) is more assertive: “it existed and was removed on purpose”. The question that matters is not technical, it is what each one signals to Googlebot.
| Aspect | 404 Not Found | 410 Gone |
|---|---|---|
| Meaning | Does not exist at this URL (or the server won’t reveal it). | It existed and was permanently removed. |
| Intent | Could be a wrong route or something temporary. | An explicit “it’s not coming back”. |
| Cacheable by default | Yes | Yes |
| What Googlebot does | Removes it from the index over time. | Removes it from the index over time, Google says it treats this the same as 404. |
304 Not Modified: the exchange that saves bandwidth
The 304 Not Modified (§15.4.5) is the one 3xx that redirects nothing, it is the engine of cache revalidation. The idea: instead of re-downloading a resource that may not have changed, the client asks “did it change?” and, if the answer is no, reuses what it already has. This negotiation uses validators. The server sends an ETag (a fingerprint of the content, RFC 9110 §8.8.3) or a Last-Modified (§8.8.2). On the next visit, the client returns those values in the conditional headers If-None-Match and If-Modified-Since (conditional requests, RFC 9110 §13; caching rules, RFC 9111).
# Primeira visita: o servidor devolve o recurso + um validador
GET /static/app.css HTTP/1.1
Host: exemplo.com
HTTP/1.1 200 OK
Content-Type: text/css
Content-Length: 46080
Cache-Control: max-age=0, must-revalidate
ETag: "9f8a1c-b3d0"
Last-Modified: Tue, 07 Jul 2026 10:00:00 GMT
...46080 bytes de CSS...
# Recarga: o cliente pergunta "mudou?" com o validador que guardou
GET /static/app.css HTTP/1.1
Host: exemplo.com
If-None-Match: "9f8a1c-b3d0"
If-Modified-Since: Tue, 07 Jul 2026 10:00:00 GMT
# Nada mudou -> resposta sem corpo
HTTP/1.1 304 Not Modified
ETag: "9f8a1c-b3d0"
Cache-Control: max-age=0, must-revalidate
Date: Wed, 08 Jul 2026 09:00:00 GMT
(sem corpo / no body)Worked example #2: suppose a 45 KB CSS file, that is, 46,080 bytes (45 × 1,024). On the first visit, the client downloads the 46,080 bytes and stores the ETag "9f8a1c-b3d0". On reload, it sends If-None-Match with that value. If nothing changed, the server responds 304 returning only the headers, about 180 bytes, no body. Instead of 46,080 bytes, 180 travel: a saving of 45,900 bytes (46,080 − 180), or 99.6% (45,900 ÷ 46,080). Multiply that across dozens of resources per page and thousands of visits, and 304 becomes one of the web’s biggest performance levers.
View the data
| Category | Value |
|---|---|
| 200 OK (full body) | 46,080 bytes |
| 304 Not Modified (revalidation) | 180 bytes |
Two details close the topic. First, a 304 never carries a body, if a proxy or framework returns content in a 304, something is wrong. Second, ETags come in two forms: strong (the content is byte-for-byte identical) and weak (a W/ prefix, the content is equivalent enough). The weak form helps when compression or whitespace vary without changing meaning. In both cases, it is the server that decides whether the validator still holds.
5xx: the other side failed, 500 vs 503, with 429 in the middle
In 5xx the problem is the server’s, but recognizing which code appeared points the debugging. The 500 Internal Server Error (§15.6.1) is the generic one, almost always an unhandled exception; the useful answer is in the log, not the number. The 502 Bad Gateway and 504 Gateway Timeout involve a proxy: 502 is an invalid response from the upstream service (the app crashed), 504 is the proxy getting no answer in time (the app hung). And the 503 Service Unavailable (§15.6.4) is the most honest of the family.
| Code | What it usually is | Temporary? |
|---|---|---|
| 500 Internal Server Error | Unhandled exception; a bug in the app. | Says nothing about retrying |
| 502 Bad Gateway | Invalid upstream response (app crashed). | Maybe |
| 503 Service Unavailable | Momentary unavailability (deploy, overload). | Yes, accepts Retry-After |
| 504 Gateway Timeout | The upstream took too long (a stuck query). | Maybe |
The practical difference between 503 and 500 is what the client should do. The 500 promises nothing, it could be a permanent bug. The 503 says “I’m down right now, come back later” and can attach the Retry-After header (RFC 9110 §10.2.3) telling you when to try again. That same header shows up in the 4xx cousin that signals excess: 429 Too Many Requests (RFC 6585 §4). When a rate limit is hit, a well-behaved server responds 429 and suggests in Retry-After how long to wait; a well-behaved client obeys before retrying.
Choosing the right code (and the curious corners)
Choosing the right status is a design decision, not a last-minute detail. The code is the first thing another system reads, a proxy, a crawler, a client deciding whether to retry. Getting it wrong is lying to whoever trusts that reading. This checklist settles most cases:
- Moved a plain page (GET) permanently? 301. Is it an API endpoint that receives POST? 308 (preserves the method).
- A passing detour? 302. If the method must survive, 307.
- Missing login/token? 401, and never forget the WWW-Authenticate header. Logged in but blocked? 403.
- A page that does not exist? 404. Removed on purpose? 410. Never 200 with “not found” in the body.
- Rate limit hit? 429 with Retry-After. Down for deploy/overload? 503 with Retry-After.
- Unexpected backend exception? 500, and go straight to the log. Cached resource might still be valid? Emit an ETag and answer 304 on revalidation.
Why 307 and 308 were born
The original intent of 301 and 302 was for the client to repeat the same method at the new URL. But browsers, for interoperability, started turning POST into GET when following these redirects, and the practice became the de facto rule. RFC 9110 merely records that historical note. Since 301 and 302 could not be “fixed” without breaking the whole web, two new, unambiguous codes were created: 307 (temporary) and 308 (permanent), both with a single promise, do not touch the method or the body.
The soft 404, in detail
A soft 404 is not a code, it is Google’s diagnosis for a 200 response whose content says “not found” or is empty. It costs you twice: it burns crawl budget on URLs that should disappear and pollutes the index with worthless pages. The fix is simple and lives on the server, not in the HTML: make the nonexistent route return 404 (or 410) on the status line. A framework that renders a pretty error screen with HTTP 200 is the number-one cause of this.
418 I’m a teapot: the joke that became reserved
The 418 comes from RFC 2324, the Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0), published on 1 April 1998, an April Fools’ joke. The rule: any attempt to brew coffee with a teapot must result in “418 I’m a teapot”. RFC 7168 (HTCPCP-TEA, 2014) even extended the joke to tea. None of it is serious standardization, but IANA’s official registry marks 418 as (Unused) and RFC 9110 §15.5.19 reserves it, precisely so nobody reuses it and breaks the joke. In practice, some services use it as an easter egg or a block response.
429 and exponential backoff
When you get a 429, the first rule is to honor Retry-After if it is present. If it is not, do not keep hammering the server: back off exponentially, wait 1 s, then 2 s, 4 s, 8 s, and add a bit of randomness (jitter) so that many clients that failed together do not all return at the same instant and topple the service again. A maximum wait cap avoids absurd delays. It is the difference between a client that helps the server recover and one that joins the pile-on.
Reading the status is just the start; the headers around it complete the story. Once the code is right, audit the rest of the response: the security headers analyzer grades your headers, the cookie inspector breaks down Set-Cookie flags, and the link checker vets a URL’s safety before you open it. Together, they turn “what number was that?” into “what is the whole response saying?”.
Frequently asked questions
301 or 302 to change a page’s URL?
When should I use 307/308 instead of 302/301?
What is the difference between 401 and 403?
404 or 410 for a removed page? Does Google treat them differently?
What is a soft 404 and why is it a problem?
What to do when you get a 429?
Is 418 “I’m a teapot” a real code?
Every HTTP code is a decision. Read the first digit to know which side owns the problem; choose between 301/308 and 302/307 by asking “must the method survive?”; use 401 (with WWW-Authenticate) for identity and 403 for permission; return a real 404/410 and never a disguised 200; and let 304 with an ETag save bandwidth. With RFC 9110 as your reference, the right status stops being memorization and becomes part of the design.
Sources & references
- RFC 9110, HTTP Semantics (status codes, Retry-After, WWW-Authenticate)
- RFC 9111, HTTP Caching (validation, ETag, 304)
- RFC 6585, Additional HTTP Status Codes (428, 429, 431, 511)
- RFC 7538, 308 Permanent Redirect
- RFC 2324, HTCPCP/1.0 (the origin of 418)
- IANA, Official HTTP Status Code Registry
- Google Search Central, HTTP status codes and soft 404s