Networking

HTTP status codes: the decisions each number carries

Every HTTP request comes back with a three-digit number, and MDN already lists them all better than any article could. What nobody organizes is the part that matters when you are debugging: each number is an engineering decision with real consequences. Returning 302 where it should be 307 breaks a POST in production. Returning 200 on a page that does not exist fools Googlebot and sinks your SEO. Forgetting the WWW-Authenticate header on a 401 violates the spec itself. This guide walks the pairs everyone confuses and shows the choice each code carries, with RFC 9110 (which in June 2022 replaced the 7230–7235 series) in hand throughout. Use the [HTTP status reference](tool:http-status-checker) to look up any code as you read.

J-Kit17 min readIntermediate
  • HTTP
  • Status codes
  • API
  • SEO
  • Caching

Key takeaways

  • The first digit tells which side owns the problem: 4xx is the client, 5xx is the server. It is the fastest triage there is.
  • 301 and 302 may turn POST into GET; 307 and 308 preserve the method and body. Swapping one for the other breaks APIs.
  • 401 is “I don’t know who you are” and REQUIRES the WWW-Authenticate header; 403 is “I know who you are and you can’t”.
  • A missing page that returns 200 is a soft 404, an SEO bug. Return a real 404 or 410.

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.

The five status classes and the semantic meaning of each (RFC 9110 §15).
ClassMeaningWhose problem
1xxInformational: a provisional response, the request continues.Nobody’s, it is an intermediate signal.
2xxSuccess: the request was received, understood and accepted.Nobody’s, it worked.
3xxRedirection: further action is needed (go to another URL, use the cache).Shared: follow the instruction.
4xxClient error: syntax, authentication, permission or resource.Your request, fix what you sent.
5xxServer 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.

  1. 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).

  2. January 1997RFC 2068, HTTP/1.1

    The first standardized cut of HTTP/1.1, with persistent connections and more codes.

  3. June 1999RFC 2616, HTTP/1.1 revised

    The revision that stood as the reference for fifteen years, in a single monolithic document.

  4. June 2014RFC 7230–7235

    RFC 2616 is split into six documents (syntax, semantics, conditionals, range, caching, authentication).

  5. 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.

The four redirects everyone mixes up. Method preserved, default cacheability (RFC 9110 §15.1) and what the crawler does.
CodeDurationMethod preserved?Cacheable by default?What the crawler does
301 Moved PermanentlyPermanentNo, may turn POST→GETYesConsolidates signals to the new URL
308 Permanent RedirectPermanentYes, keeps method and bodyYesConsolidates signals to the new URL
302 FoundTemporaryNo, may turn POST→GETNoKeeps the original URL indexed
307 Temporary RedirectTemporaryYes, keeps method and bodyNoKeeps 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=cartao
With 302, the POST becomes GET and the body vanishes. With 307, method and body survive the redirect. For a permanent API endpoint migration, the same logic holds between 301 and 308.

The 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.

Search a code (e.g. 308) and see its meaning, RFC, default cacheability, common causes and how to handle it, all in the browser.Open the tool full page

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.

404 vs 410: what changes for the backend and for the crawler.
Aspect404 Not Found410 Gone
MeaningDoes not exist at this URL (or the server won’t reveal it).It existed and was permanently removed.
IntentCould be a wrong route or something temporary.An explicit “it’s not coming back”.
Cacheable by defaultYesYes
What Googlebot doesRemoves 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)
The conditional request with If-None-Match and the 304 response. The server confirms the ETag still matches and does not resend the 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.

200 OK (full body)46,080 bytes
304 Not Modified (revalidation)180 bytes
Bytes on the wire for the same 45 KB file: the full response (200) versus the revalidation (304). The 304’s tiny bar is the whole argument.
View the data
CategoryValue
200 OK (full body)46,080 bytes
304 Not Modified (revalidation)180 bytes
46,080 → 180bytes: full response vs. 304 revalidation
~99.6%bandwidth saved on this revalidation
0body bytes in a 304 (forbidden by RFC 9110 §15.4.5)

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.

The four most common server errors and what each usually means.
CodeWhat it usually isTemporary?
500 Internal Server ErrorUnhandled exception; a bug in the app.Says nothing about retrying
502 Bad GatewayInvalid upstream response (app crashed).Maybe
503 Service UnavailableMomentary unavailability (deploy, overload).Yes, accepts Retry-After
504 Gateway TimeoutThe 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?
If the change is permanent, use 301: it is permanent, cacheable by default and transfers SEO authority to the new URL (Google treats 301 and 308 as equivalent). 302 is for temporary detours and keeps the old URL indexed. Just mind the method: both 301 and 302 may turn a POST into GET.
When should I use 307/308 instead of 302/301?
Whenever the method and body must survive the redirect. 307 (temporary) and 308 (permanent) preserve the POST; 302 and 301 may convert it to GET and drop the body. In APIs, redirecting a POST with 302 is a classic bug: use 307 for passing detours and 308 to permanently move an endpoint.
What is the difference between 401 and 403?
401 means you are not authenticated, “I don’t know who you are”, and RFC 9110 §15.5.2 requires the response to carry the WWW-Authenticate header. 403 means you are authenticated but lack permission, “I know who you are and you can’t”. Re-authenticating may fix a 401; it won’t fix a 403.
404 or 410 for a removed page? Does Google treat them differently?
Use 410 Gone when you want to declare the removal is permanent; 404 Not Found means “not here”, which may be temporary. Despite the folklore that 410 leaves the index faster, Google’s documentation says it treats all 4xx (except 429) the same: both lead to removal over time. The serious mistake is returning 200 on a nonexistent page, which creates a soft 404.
What is a soft 404 and why is it a problem?
It is when a page that does not exist responds with HTTP 200, but the body says “not found” or is empty. Search Console flags it as a soft 404. It is a problem because it wastes crawl budget and pollutes the index with worthless pages. The fix is to serve the right status on the status line: a real 404 or 410, not a disguised 200.
What to do when you get a 429?
You hit a request limit (RFC 6585 §4). Honor the Retry-After header if present, it comes in seconds or as an HTTP date (RFC 9110 §10.2.3). If it is absent, apply exponential backoff (1 s, 2 s, 4 s...) with a bit of jitter so clients don’t synchronize, and set a maximum wait. Never keep retrying at the same pace.
Is 418 “I’m a teapot” a real code?
It is real as a registration, but it was born from a joke. It comes from RFC 2324 (HTCPCP), published on 1 April 1998. It is not part of serious HTTP, but IANA marks it as (Unused) and RFC 9110 §15.5.19 reserves it to preserve the joke. Some services use it as an easter egg. Do not return 418 for real errors, use the code that describes what happened.

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

  1. RFC 9110, HTTP Semantics (status codes, Retry-After, WWW-Authenticate)
  2. RFC 9111, HTTP Caching (validation, ETag, 304)
  3. RFC 6585, Additional HTTP Status Codes (428, 429, 431, 511)
  4. RFC 7538, 308 Permanent Redirect
  5. RFC 2324, HTCPCP/1.0 (the origin of 418)
  6. IANA, Official HTTP Status Code Registry
  7. Google Search Central, HTTP status codes and soft 404s