Skip to main content
HTTP Status Codes Explained: 301 vs 302, 404 vs 410
On this page

HTTP Status Codes Explained: 301 vs 302, 404 vs 410

16 September 2026

Two status codes are famous. 404 means the page is not there, 500 means the server fell over, and most people stop learning at that point. The trouble is that neither of those is the code that quietly costs you anything. The expensive ones are the codes nobody looks at: a 302 where you meant a 301, a 200 on a page whose text says "sorry, nothing here", a 503 on your robots.txt that stops Google crawling the entire site for twelve hours. All three look fine in a browser. All three change what search engines, caches and other machines do with your site.

This article explains what a status code actually is, where the three-digit shape came from, and what each code makes the other side do. It covers the five redirects and why they are not interchangeable, the difference between 404, 410 and a soft 404, the conditional and partial-content codes that make caching work, and it ends with the complete IANA register of every assigned code so you never have to guess what a number in a log file means.

The three digits at the top of every HTTP response, and what each one asks the other side to do.

Goal: after reading this you can look at any status code, say what it makes a browser, a cache and a crawler do, and choose the right one when you are the server.

1. The Basics

An HTTP status code is a three-digit number the server puts at the front of every response. It is the answer to the question "what happened to my request", and it arrives before any of the content does. Your browser reads it, decides what to do, and only then thinks about the body.

That order matters more than it sounds. The status code is not a description of the page. It is an instruction to the client about what to do next: render this, go over there instead, use your cached copy, ask again with credentials, wait and retry, give up. The words on the page are for humans. The number is the only part that machines obey.

1.1 Where the Code Sits

Ask any server for just the headers and the first line you get back is the status line:

$ curl -sI https://petermartin.nl/ | head -3
HTTP/2 301
server: nginx-rc
location: https://petermartin.nl/en/

The first line is the status line, and it holds two things:

HTTP/2 301
  |    |
  |    └─ the status code: three digits, and the only part machines read
  └───── the protocol version that carried it

That is a complete answer already. The server said "the thing you asked for lives at a different address, permanently", and it named the address in the Location header. No content was sent, and none was needed. A browser reading this makes a second request to /en/ without showing the user anything.

The right mental model: a status code is a verb, not a label. It does not tell the client what the resource is; it tells the client what to do. Every argument about which code to use is really an argument about what you want the other machine to do next.

1.2 The Five Classes

The first digit is the class. RFC 9110 (June 2022), the current definition of HTTP semantics, defines five of them and says plainly that "the last two digits do not have any categorization role".

ClassNameWhat it meansWho fixes it
1xxInformationalReceived, still working. An interim answer, followed later by a real oneNobody. You will rarely see one
2xxSuccessfulThe request was received, understood and acceptedNobody
3xxRedirectionSomething else has to happen before this request completesUsually the site owner, when it is the wrong kind
4xxClient ErrorThe request was wrong, or is not allowedWhoever made the request, or whoever published the broken link
5xxServer ErrorThe request looked fine and the server failed anywayYou, or your host

The 4xx versus 5xx split is the single most useful thing in the whole system, because it decides who is responsible. A 4xx means the server is working and is rejecting what it was given. A 5xx means the server agrees the request was reasonable and could not do it. Send the first to whoever is calling you; keep the second.

1.3 Only the First Digit Is Load-Bearing

Because the class carries the meaning, a client does not need to recognise a code in order to handle it correctly. RFC 9110 requires exactly that: a client "MUST understand the class of any status code, as indicated by the first digit, and treat an unrecognized status code as being equivalent to the x00 status code of that class".

a client receives  471   (nobody has ever defined 471)
it reasons         4xx → my request was wrong
it behaves as if   400 Bad Request

valid range        100 to 599 inclusive
outside that       treat the response as a 5xx (Server Error)

This is why new status codes can be deployed on the open internet without breaking anything: a client from 2005 meeting a 451 from 2016 does not crash, it treats it as a 400 and moves on. It is also why inventing your own code inside a valid class is less dangerous than it sounds, and why doing it anyway is still a bad idea (see section 6.8).

1.4 The Reason Phrase Is Decoration

In HTTP/1.1 the status line carries a short text after the number: HTTP/1.1 404 Not Found. That text is the reason phrase, and RFC 9110 is blunt about its value: the phrases "are only recommendations" that "can be replaced by local equivalents or left out altogether without affecting the protocol".

In HTTP/2 and HTTP/3 they are not optional, they are gone. RFC 9113 defines a single :status pseudo-header carrying the code and nothing else. Look again at the output in section 1.1 and you can see it: the line reads HTTP/2 301 with nothing after the number, because there is nothing left to print.

The practical consequence: never write code that matches on the text. A proxy is allowed to change "Not Found" to "Niet gevonden", and any response delivered over HTTP/2 or HTTP/3 carries no text at all.

1.5 What This Article Covers

Status codes are one stage of a much longer chain. If you want the whole journey from typing an address to seeing pixels, that is a separate article: what actually happens when you enter a URL. This one starts at the moment the response comes back, and stays there.

Sections 1 to 3 are the system: what a code is, where the shape came from, and how the list grew. Sections 4 to 6 are use: reading codes, choosing the right redirect and the right error, and the codes the protocol uses on your behalf for caching, ranges, upgrades and retries. Section 6.9 is the complete register of every code IANA has assigned. Sections 7 to 10 are the parts that surprise people, and the mistakes worth not making.

Back to top

2. Where the Name Comes From

The term is descriptive rather than clever. The first line of an HTTP response is called the status line, the number on it is the status code, and the words after it are the reason phrase. HTTP/2 and HTTP/3 kept the code, dropped the line and the phrase, and kept the name.

What is worth knowing is that HTTP did not invent the shape. A three-digit reply whose first digit gives the class was already standard practice on the internet when the web was built. RFC 959 (October 1985) specified it for FTP in almost the same words HTTP uses today:

First digitFTP, 1985HTTP, today
1yz / 1xxPositive Preliminary replyInformational
2yz / 2xxPositive Completion replySuccessful
3yz / 3xxPositive Intermediate replyRedirection
4yz / 4xxTransient Negative Completion replyClient Error
5yz / 5xxPermanent Negative Completion replyServer Error

Two differences are worth noticing, because both still catch people out.

First, FTP splits 4yz from 5yz on temporary versus permanent. HTTP splits 4xx from 5xx on whose fault it is. That is why 429 Too Many Requests feels wrong in the 4xx block: by FTP's rule it would be a 4yz, but by HTTP's rule the client did send too many requests, so it is a client error even though the sensible response is to wait rather than to change anything. Google's crawlers quietly disagree and count it as a server error (section 7.8).

Second, FTP gave the second digit a meaning too: x0z syntax, x1z information, x2z connections, x3z authentication, x5z file system. HTTP deliberately did not. In HTTP the last two digits are just a serial number inside the class, which is why 404 and 451 have nothing in common beyond both being refusals, and why codes could be added later without needing a free slot in the right sub-range.

Back to top

3. A Short History

The best argument for status codes is the version of HTTP that did not have them.

3.1 The Protocol With No Codes

The original protocol, retrospectively called HTTP/0.9, is a page long. The client sends GET /path and a newline. The server sends HTML and closes the connection. There are no headers, no methods other than GET, and no status codes. Tim Berners-Lee's own 1991 specification states the consequence in one sentence:

"Error responses are supplied in human readable text in HTML syntax. There is no way to distinguish an error response from a satisfactory response except for the content of the text."

That sentence is the reason the rest of this article exists. A machine could not tell a page from an apology. Every caching proxy, every crawler, every retry loop and every uptime monitor needs that distinction, and none of them can get it by reading English. Notice also that the same failure is still available to you today: serve your error page with a 200 and you have voluntarily gone back to 1991 (section 5.5).

3.2 The First List

By 1992 there was a draft list of codes, and it is still online at the W3C. Reading it now is startling, because three things in it never happened.

  • 402 Payment Required was a real design, not a placeholder. The 1992 text says the response "gives a specification of charging schemes acceptable" and the client "may retry the request with a suitable ChargeTo header". There was going to be a payment header in HTTP. There never was, and 402 has now been "reserved for future use" for over thirty years.
  • 502 and 503 meant different things. In that draft 502 was "Service temporarily overloaded" and 503 was "Gateway timeout". Today 502 is Bad Gateway, 503 is Service Unavailable and Gateway Timeout has moved to 504. The meanings shifted between the draft and the standard, which is worth remembering when you read very old advice.
  • The client/server split was never meant to be reliable. The 1992 text admits it: the 4xx codes are for cases where the client seems to have erred and 5xx where the server has, but "it is impossible to distinguish these cases in general, so the difference is only informational".

3.3 The Milestones

YearDocumentWhat changed
1991HTTP as implemented in W3No status codes at all. An error is a page that happens to say so
1992W3C draft listThe first codes, including a 402 that expected a ChargeTo header
May 1996RFC 1945, HTTP/1.0Exactly 15 codes are defined. 302 is called "Moved Temporarily". No 1xx exists
January 1997RFC 2068, HTTP/1.136 codes. The 1xx class arrives, plus 206, 405 to 415 and 504. 302 is still "Moved Temporarily"
June 1999RFC 261639 codes. 302 is renamed "Found" and 307 is added beside it to repair the damage. The revision everyone quoted for the next 15 years
1998-2010RFC 2295, 2324, 2518, 2774, 3229, 4918, 5842Extensions bolt on codes for content negotiation, WebDAV, delta encoding, and one joke
April 2012RFC 6585428, 429, 431 and 511: rate limiting and captive portals finally get codes
June 2014RFC 7238, then RFC 7538 (April 2015)308 arrives as an experiment and becomes a standard, 18 years after HTTP/1.0 standardised 301
February 2016RFC 7725451, for content blocked by a legal demand
December 2017RFC 8297103 Early Hints, the first 1xx anyone had a real use for since 1997
September 2018RFC 8470425 Too Early, invented because TLS 1.3 made replay attacks possible
June 2022RFC 9110Semantics are separated from the wire format. One definition of 404 now serves HTTP/1.1, /2 and /3
2024 onwardsResumable uploads draft104 holds a temporary registration, the first new code in six years

The 2022 split is the part worth carrying with you. RFC 9110 defines what a code means; RFC 9112, 9113 and 9114 define how HTTP/1.1, HTTP/2 and HTTP/3 carry it. A 404 is the same promise on all three, which is exactly why the reason phrase could be dropped from two of them without anything breaking.

Back to top

4. Simple Use Cases: Reading the Code

Before choosing codes, you need to be able to see them. Everything in this section is one command, and none of it downloads the page.

4.1 The One Command Worth Memorising

curl -I sends a HEAD request: headers only, no body. Add -s to silence the progress meter.

$ curl -sI https://petermartin.nl/en/ | head -1
HTTP/2 200

One caveat that costs people an afternoon: a few servers and applications handle HEAD differently from GET, so a HEAD can report a code the real page never returns. When the answer surprises you, confirm it with a real GET that throws the body away:

$ curl -s -o /dev/null -w '%{http_code}\n' https://petermartin.nl/en/
200

The -w (write-out) form is the more useful of the two, because you can ask for several facts at once and get one line back:

$ curl -s -o /dev/null -L -w 'code=%{http_code} redirects=%{num_redirects} final=%{url_effective}\n' \
    https://developers.google.com/search/docs/crawling-indexing/http-network-errors
code=200 redirects=1 final=https://developers.google.com/crawling/docs/troubleshooting/http-status-codes

Read that carefully, because it contains two different answers. The final code is 200, and a monitoring tool that only records the final code would call this URL healthy. It also took a redirect to get there: Google moved its own status-code documentation to a new path and left a 301 behind. Both facts matter, and only one of them is the "status code" people usually quote.

4.2 The Four Codes That Describe Most Sites

On an ordinary content site, four codes cover almost every response. If you understand these, you can read a log file.

CodeMeaningWhat it should mean on your site
200OKHere is the page. The overwhelming majority of your traffic
301Moved PermanentlyThis address changed for good. Update your links and your bookmarks
404Not FoundNothing here. Someone linked or typed wrong, or you deleted something
500Internal Server ErrorYour application crashed. Check the error log, not the browser

Everything else in this article is a refinement of those four: a redirect that behaves differently, a "not found" that means something more specific, or a failure that points at a different machine.

4.3 Following a Redirect, Hop by Hop

The interesting part of a redirect is never the destination, it is the route. -L follows redirects and, combined with -I, prints the head of every response in the chain:

$ curl -sIL https://developers.google.com/search/docs/crawling-indexing/http-network-errors \
    | grep -Ei '^HTTP/|^location'
HTTP/2 301
location: /crawling/docs/troubleshooting/http-status-codes
HTTP/2 200

That is a healthy redirect: one hop, permanent, straight to a 200. What you are looking for is the opposite shape - three or four hops, a mix of 301 and 302, a hop through http://, or a chain that ends in a 404. Each of those is a real cost, and section 5.4 covers what breaks.

4.4 Checking a List of URLs

After a migration you do not want one answer, you want a few hundred. A three-line shell loop is enough, and the output sorts and counts:

$ while read -r u; do
    printf '%s %s\n' "$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$u")" "$u"
  done < urls.txt
301 https://petermartin.nl/
200 https://petermartin.nl/en/
200 https://petermartin.nl/en/focus-on/web/dns
404 https://petermartin.nl/this-page-is-gone

Note that the site's own home page answers 301, redirecting to the language folder. That is normal and intended, but it is exactly the kind of thing you want to see written down before you change a template. Deliberate redirects and accidental ones look identical in a log.

4.5 Reading Codes You Did Not Request

Two other places show you status codes for traffic you never sent yourself.

  • Your access log. Every request your server answered, with the code it answered with. Grouping a day of log lines by code tells you more about a site's health than any dashboard: a sudden bloom of 404s means a bad deploy or a link rot problem, a bloom of 499s or 5xxs means the application is falling behind.
  • Browser developer tools. The Network tab lists every request the page made, with its status. This is where you find the failing background request that the page itself hides: a 200 page that pulls in a 404 script still looks fine until something needs that script.

For the crawler's view of the same thing - which codes Googlebot met, on which of your URLs, and what it did about them - see how to read the Search Console reports correctly.

Back to top

5. Moderate Use Cases: The Codes You Choose

Reading codes is diagnostics. Choosing them is design. Every code in this section is one you decide to send, and the decision has consequences that outlive the request.

5.1 The Five Redirects Are Not Interchangeable

There are five redirect codes in normal use, and almost everyone uses two of them for everything. The differences are real, and they are not about style. Each answer differs on three questions: does the client keep the request method, may a cache remember the redirect without being told, and what does a search engine learn from it.

CodeNameMethod kept?Cacheable by default?Google treats it as
301Moved PermanentlyNo. A client MAY turn POST into GETYesA strong signal for the target
302FoundNo. A client MAY turn POST into GETNoA weak signal for the target
303See OtherNo. The client makes a fresh GET, by designNoA weak signal, same as 302
307Temporary RedirectYes. The client MUST NOT change itNoA weak signal, same as 302
308Permanent RedirectYes. The client MUST NOT change itYesA strong signal, same as 301

The Google column is from Google's own documentation on how status codes affect its crawlers, checked 7 September 2026. Google's advice on the pairs is worth quoting because it is easy to misread as permission to be sloppy: it says 308 is equivalent to 301 and 307 to 302 for its purposes, and then adds that you should still "use the status code that's appropriate for the redirect so other clients (for example, e-readers, other search engines) may benefit from it".

303 is the one redirect with a named design pattern behind it. Post/Redirect/Get is the standard cure for the "confirm form resubmission" dialog: the browser POSTs the form, the server answers 303, and the browser fetches the confirmation page with a fresh GET. The address bar then holds a URL that is safe to reload, bookmark and share, and refreshing it cannot submit the form again. RFC 9110 names exactly this use: 303 is "primarily used to allow the output of a POST action to redirect the user agent to a different resource".

5.2 The Method Rewriting Trap

This is the part of the table that breaks things, and it is a historical accident. RFC 9110 documents it as such: for both 301 and 302, "for historical reasons, a user agent MAY change the request method from POST to GET for the subsequent request".

In 1996 that was not the intent, but every browser did it, so the standard eventually described reality and added 307 and 308 as the versions that keep their promise. The result is a genuine hazard:

a form POSTs to  /api/subscribe
the server sends 301  →  /api/v2/subscribe
the browser then GETs /api/v2/subscribe    ... with no form data

same redirect as 308:
the browser then POSTs /api/v2/subscribe   ... with the form data intact

Your own tools do the same thing. The curl manual states it directly: when curl follows a redirect and the request is a POST, "it sends the following request with a GET if the HTTP response was 301, 302, or 303. If the response code was any other 3xx code, curl resends the following request using the same unmodified method".

The practical rule: if a URL is only ever fetched, any redirect works; if it can be posted to, use 308 or 307. Redirecting an API endpoint or a form target with a 301 silently discards request bodies, and the symptom is not an error - it is a form that appears to submit and does nothing.

5.3 A 301 Is Much Harder to Take Back Than It Looks

A 301 is heuristically cacheable, which is the polite way of saying that browsers and proxies are allowed to remember it without being told how long for. In practice browsers remember them aggressively, and the visitor has no obvious way to undo it.

Two consequences follow. First, never use a 301 to test a redirect - use 302 until you are certain, then switch. Second, if you must publish a permanent redirect you might reverse, send an explicit cache lifetime with it, because "heuristically cacheable" only applies when you say nothing:

HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-page
Cache-Control: max-age=3600      ── one hour, not "until the browser is reinstalled"

5.4 Chains, Loops and Hop Limits

Every client gives up after some number of redirects, and the numbers are not the same. These are the current limits, verified 7 September 2026:

ClientLimitSource
Chrome and Firefox20 hopsChromium's kMaxRedirects and Firefox's network.http.redirection-limit, both following the WHATWG Fetch specification
curl50 hopsThe default for --max-redirs
Googlebot, normal crawling10 hopsGoogle's crawler documentation
Googlebot, fetching robots.txtat least 5, then it is treated as a 404Google's robots.txt specification
Google Inspection Tools0. It does not follow redirects at allGoogle's crawler documentation

The limits are generous enough that reaching them means something is wrong. The real damage from chains happens long before the limit: every hop is a full round trip, so a three-hop chain on a mobile connection can cost half a second before the first byte of the actual page. Chains also decay. Redirect A points at B, B points at C, then someone deletes C, and now the whole chain ends in a 404 that nothing in your CMS shows you.

Two rules keep this manageable. Always redirect to the final destination, never to another redirect - when you add a rule, check whether an existing rule already points at the URL you are leaving. And redirect to https:// directly, never through an http:// hop you control, because the first hop of a chain is the one that travels unencrypted.

5.5 404, 410, and the Soft 404

These three are the same event with three different answers, and only one of them is a mistake.

  • 404 Not Found means the server has nothing for this address and is not saying whether that is permanent. It is the right default: it covers typos, dead links, and things that might come back.
  • 410 Gone means you know the resource is deliberately and permanently gone. RFC 9110 prefers it "if the origin server knows, presumably through some configurable means, that the condition is likely to be permanent", and its stated purpose is to tell the recipient that "remote links to that resource be removed".
  • A soft 404 is not a status code. It is a page that says "not found" in words while the status line says 200 OK. This is the failure that HTTP/0.9 had no way to avoid, reintroduced on purpose.

Google's documentation defines the soft 404 exactly that way: if a 2xx response's content "suggests an error for Google Search, an empty page or an error message, Search Console will show a soft 404 error". The cost is real. A soft 404 is a page Google spends crawl budget on, may index, and must guess about, where a real 404 is a fact it can act on immediately.

Be careful with one popular piece of advice, though. It is often claimed that 410 gets content removed from Google faster than 404. Google's current documentation says otherwise: all 4xx codes except 429 are "treated the same", the URL is removed from the index if it was previously indexed, and crawl frequency gradually decreases. Use 410 because it is the honest answer for other clients and for your own logs, not because you expect a different result from Google.

The practical failure mode is worth naming: a CMS that renders a "page not found" template inside the normal layout, with the normal 200. Verify it, because you cannot see this in a browser:

$ curl -s -o /dev/null -w '%{http_code}\n' https://petermartin.nl/this-page-is-gone
404          ── correct. A soft 404 would print 200 here

5.6 401 Versus 403

Both refuse. They differ on whether trying again could possibly help.

401 Unauthorized403 Forbidden
MeansYou have not proved who you areI know who you are, and the answer is still no
Required headerWWW-Authenticate - the server MUST send itNone
Should the client retry?Yes, with credentialsNot with the same credentials
Typical causeMissing or expired token, no HTTP auth headerWrong role, IP block, WAF rule, file permissions

The name is the confusing part: 401 is called "Unauthorized" but means unauthenticated, and 403 is the one that is really about authorisation. RFC 9110 also allows a third option that is sometimes the right one: a server that does not want to admit a forbidden resource exists "MAY instead respond with a status code of 404".

That option is a security tool rather than a courtesy, because the pair of codes you return is itself information. If a request for something that does not exist answers 404, while a request for something that exists but is not yours answers 403, anyone can map what exists just by reading the codes:

/users/does-not-exist/private   → 404    "no such user"
/users/real-account/private     → 403    "that one exists, but not for you"

The same leak shows up in login forms that distinguish "unknown username" from "wrong password". Where that matters, make the two cases indistinguishable from outside - answer 404 to both - and keep the precise reason in your logs, where the attacker cannot read it. The most informative response is not always the safest one to publish.

One warning from Google's crawler documentation, because it is a mistake people make on purpose: "Don't use 401 and 403 status codes for limiting the crawl rate." Those codes have no effect on crawl rate; they just remove your pages from the index.

5.7 429 and Retry-After

429 Too Many Requests (RFC 6585, April 2012) is the polite way to rate limit. What makes it useful is not the code but the header that should accompany it:

HTTP/1.1 429 Too Many Requests
Retry-After: 3600
Content-Type: text/html

Retry-After takes either a number of seconds or an HTTP date, and it turns an error into an instruction. Without it, a well-written client has to guess, and a badly written one will simply retry immediately and make the overload worse.

Two rules that are easy to get wrong. A 429 "MUST NOT be stored by a cache" - it is about this client right now, not about the resource. And the response body should say what the limit is, because the human debugging the client is not the person who wrote it.

5.8 503 Is the Only Honest Code for "Down Right Now"

When you take a site down for maintenance, or the application cannot reach its database, the correct answer is 503 Service Unavailable with a Retry-After. The alternatives are all worse:

  • A 200 with a "we will be back soon" page invites every crawler to index "we will be back soon" as the content of every URL on the site.
  • A 302 to a maintenance page moves the problem, and now you have to remember to remove it.
  • A 404 tells crawlers the pages are gone, which is a lie you will pay for later.

Google's crawler documentation describes what a 503 buys you: 5xx responses make Google's crawlers "temporarily slow down with crawling", already-indexed URLs are preserved in the index "but eventually dropped", and once the server responds 2xx again Google "gradually increases the crawl rate". A maintenance window measured in hours is safe. One measured in weeks is not.

5.9 500, 502 and 504 Point at Three Different Machines

All three mean "the server failed", but they are emitted by different parts of the stack, and knowing which narrows the search enormously.

CodeWho sent itWhat it usually meansWhere to look
500Your applicationThe code ran and threw. A fatal error, an uncaught exception, a bad configurationThe application error log
502A gateway in front of itThe gateway reached the application and got an invalid response, or none. The process died or is not listeningWhether the application process is running, and the socket or port it listens on
504A gateway in front of itThe gateway waited and gave up. The application is alive but too slowSlow queries, external API calls, and the gateway's timeout setting

On a typical stack of a web server in front of an application server, the distinction is very practical: 502 means the application is not answering at all, 504 means it is answering too slowly, and 500 means it answered with a failure. Only the last one will have a useful stack trace.

Back to top

6. Advanced Use Cases: The Codes the Protocol Uses

The codes in section 5 are the ones you configure. The ones below are mostly emitted by the protocol on your behalf, by caches, by proxies, and by APIs. You will not choose them often, but you will meet them, and each one exists because something specific goes wrong without it.

6.1 Conditional Requests: 304 and Friends

Caching is not "the browser stores the file and stops asking". Most of the time the browser does ask, and the server answers with a 304 Not Modified: a full request and response that carries no content at all.

First, a normal request. Notice the etag, which is the server's opaque fingerprint for this exact version of the file:

$ curl -sI https://petermartin.nl/media/vendor/joomla-custom-elements/css/joomla-alert.min.css
HTTP/2 200
content-type: text/css
content-length: 3654
last-modified: Tue, 18 Aug 2026 15:09:56 GMT
etag: "6a8475c4-e46"
cache-control: max-age=2592000

Now ask again, but say which version you already hold:

$ curl -sI -H 'If-None-Match: "6a8475c4-e46"' \
    https://petermartin.nl/media/vendor/joomla-custom-elements/css/joomla-alert.min.css
HTTP/2 304
last-modified: Tue, 18 Aug 2026 15:09:56 GMT
etag: "6a8475c4-e46"
cache-control: max-age=2592000
                          (and no content-length, because there is no content)

3654 bytes became zero. The round trip still happened, so a 304 is not free - on a slow connection the latency is most of the cost anyway - but nothing was transferred. There are two ways to trigger it: If-None-Match with an ETag, which is exact, and If-Modified-Since with a date, which is cruder and only has one-second resolution.

Three more codes belong to this family:

CodeWhen it appears
412 Precondition FailedYou sent If-Match with a write request and the resource had changed underneath you. The write was refused rather than silently overwriting someone else's work
428 Precondition RequiredThe server refuses unconditional writes at all. RFC 6585 introduced it for exactly the "lost update" problem: fetch, edit, save, while a third party saved in between
204 No ContentThe write succeeded and there is nothing to send back. Not an error, and not the same as an empty 200

One detail catches people writing their own caching layer: RFC 9110 requires a 304 to repeat the header fields a 200 would have sent for Content-Location, Date, ETag, Vary, Cache-Control and Expires. A 304 that drops the ETag forces the client to re-download next time, which defeats the purpose.

6.2 Partial Content: 206 and 416

When a client asks for part of a file - seeking in a video, resuming an interrupted download, or a download manager fetching four chunks at once - it sends a Range header, and a server that supports it answers 206 Partial Content:

$ curl -sI -r 0-49 https://petermartin.nl/media/vendor/joomla-custom-elements/css/joomla-alert.min.css
HTTP/2 206
content-length: 50
etag: "6a8475c4-e46"

Ask for bytes that do not exist and you get the matching refusal:

$ curl -sI -r 999999999-1000000000 \
    https://petermartin.nl/media/vendor/joomla-custom-elements/css/joomla-alert.min.css
HTTP/2 416

416 Range Not Satisfiable is worth recognising because of where it shows up: video that will not scrub, and large downloads that fail only when resumed. Both are range problems, and both are invisible in a normal page load.

6.3 The Interim Responses: 100, 101 and 103

A single request can have several responses: any number of 1xx interim ones, then exactly one final response. They carry no body and are usually invisible.

  • 100 Continue answers a client that sent Expect: 100-continue and is asking permission before uploading a large body. It saves you from streaming a 2 GB file to a server that was going to reject it on the first header.
  • 101 Switching Protocols is how a connection stops being HTTP. It is the second half of the WebSocket handshake, and the server MUST name the new protocol in an Upgrade header.
  • 103 Early Hints (RFC 8297, December 2017) is the interesting one. The server sends Link headers for resources it knows the page will need while it is still building the page, so the browser can start fetching the stylesheet during the seconds the application spends on the database. It is still an Experimental RFC, and browsers implement it with caveats: most accept early hints only over HTTP/2 or later, and process only the first one they receive.

One rule limits all three: a 1xx must never be sent to an HTTP/1.0 client, because HTTP/1.0 defined none and would not know what to do with it.

6.4 The Codes an API Owes Its Callers

A web page can survive on four codes. An API cannot, because its clients are programs that have to branch on the answer.

CodeUse it forDetail that matters
201 CreatedA successful POST or PUT that made somethingSend a Location header naming the new resource
202 AcceptedQueued for later processingDeliberately noncommittal. Point at a status resource, because HTTP has no way to report back later
204 No ContentSuccess with nothing to return, such as a deleteIt cannot carry a body at all, not even an empty JSON object
400 Bad RequestMalformed request the server cannot parseNot the catch-all it is usually used as
409 ConflictValid request that clashes with current stateThe body should carry enough to resolve the clash
422 Unprocessable ContentWell-formed request, semantically wrong contentThe right code for validation errors, where 400 is usually used
429 Too Many RequestsRate limit reachedAlways with Retry-After

The 400 versus 422 line is the one worth holding to: 400 means "I could not understand this", 422 means "I understood it perfectly and the content is wrong". A client can retry the second one after fixing a field; retrying the first without changing the shape of the request is pointless.

Even so, a status code is deliberately coarse, and an API usually has more to say than one of 64 numbers can carry. The standard place for the rest is RFC 9457 (July 2023), Problem Details for HTTP APIs, which replaced RFC 7807. Rather than every API inventing its own error shape, it defines one JSON document, sent under the media type application/problem+json:

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type":     "https://example.com/problems/invalid-email",
  "title":    "Invalid email address",
  "status":   422,
  "detail":   "The supplied address has no domain part.",
  "instance": "/subscriptions/8814"
}

Each member has one job. type is a URI naming the kind of problem, and RFC 9457 says consumers "MUST use the type URI... as the problem type's primary identifier" - so that, not the text, is what a client branches on. title is a short human-readable summary of that kind and should not change between occurrences. detail explains this particular occurrence and should help the caller fix it rather than carry debugging output. instance identifies the occurrence itself. You may add your own members beside them.

status repeats the HTTP code, and the RFC is careful about why: it is "only advisory", and generators "MUST use the same status code in the actual HTTP response, to assure that generic HTTP software that does not understand this format still behaves correctly". It is there so the original code survives when an intermediary rewrites it or when the body is logged without its response.

That division of labour settles most arguments about which code an API should return: the status code carries the protocol-level meaning that generic infrastructure acts on, and the body carries the application-specific detail. A cache, a proxy and a retry loop can all behave correctly on the 422 without understanding a word about email addresses.

6.5 The Codes That Come From the Middle

Some codes are never sent by your application, because they are answers about the connection rather than about the resource.

  • 421 Misdirected Request means the request arrived at a server that cannot answer authoritatively for that hostname. With HTTP/2 connection reuse, a browser may send a request for one domain over a connection it opened for another, and this is the correct way to say "not here, open a new connection". RFC 9110 forbids a proxy from generating it.
  • 425 Too Early (RFC 8470, September 2018) exists because of TLS 1.3. Its 0-RTT feature lets a client send data with the very first packet, which is fast and replayable by an attacker. A server that will not take that risk for this request answers 425 and the client retries once the handshake is complete.
  • 426 Upgrade Required refuses the request until the client speaks a different protocol - in practice, a plaintext client being told to come back over TLS.
  • 451 Unavailable For Legal Reasons (RFC 7725, February 2016) says the refusal is a legal demand, not a technical one. The RFC asks for a Link header with rel="blocked-by" identifying the entity doing the blocking, and notes that the code "implies neither the existence nor nonexistence of the resource". The number is a reference to Ray Bradbury's Fahrenheit 451.
  • 511 Network Authentication Required is the captive portal code: the hotel wifi, not the website. RFC 6585 says origin servers SHOULD NOT generate it, because it is for intercepting proxies.

6.6 Which Codes Are Safe to Retry

For a failed request, the status code is usually an instruction about trying again: come back later, or do not bother. Automated clients act on that instruction thousands of times an hour, and getting it wrong is how one slow minute becomes an outage - every client that retries immediately adds load to a server that already said it had too much.

CodeRetry?On what terms
408YesThe request never fully arrived. RFC 9110: if the client "has an outstanding request in transit, it MAY repeat that request"
421YesOver a fresh connection. The one code that may be retried "whether or not the request method is idempotent" (section 6.5)
425YesRequired, in fact: clients that use TLS 1.3 early data "MUST retry requests upon receipt of a 425"
429YesAfter Retry-After, and not before
503YesAfter Retry-After. This is the code that means "later", so honour the number
502, 504CarefullyThe connection failed, but the work may still have happened. See below
500RarelyThe application ran and threw. A second identical request usually throws again, so retrying mostly multiplies the log entries
400, 404, 405, 409, 410, 414, 415, 422NoNothing about the situation changes by repeating. Retry only after changing the request
401, 403Only once401 after obtaining credentials; 403 not with the same credentials at all

502 and 504 are the dangerous pair, and the danger is not obvious. Both are reported by a gateway that did not get a usable answer from upstream, which tells you nothing about whether upstream did the work. A 504 can mean the application never saw the request, or that it processed the order, charged the card and took too long to say so. Retry that automatically and you have charged the card twice.

A status code tells you whether the server would like another attempt. It does not tell you whether another attempt is safe. That is a property of the request method, not of the response.

RFC 9110 draws the line at the method: a client "SHOULD NOT automatically retry a request with a non-idempotent method unless it has some means to know that the request semantics are actually idempotent... or some means to detect that the original request was never applied". Which methods carry that promise, and how to give a POST one with an idempotency key, is the subject of methods rather than status codes, so this article stops at the boundary. Two hard rules from the same section are worth carrying across it: "A proxy MUST NOT automatically retry non-idempotent requests", and "A client SHOULD NOT automatically retry a failed automatic retry". The second is the one that prevents a retry storm.

When you do retry, the order of precedence is simple, and only the first line of it comes from the specification:

RETRY LADDER
  Retry-After present   obey it. The server has told you the answer
  no Retry-After        back off exponentially: 1s, 2s, 4s, 8s, 16s
  every attempt         add random jitter, so clients do not resynchronise
  always                cap the attempts AND set a total deadline
  after a failed retry  stop. Escalate to a human or a queue

  Retry-After is defined by RFC 9110. Backoff, jitter and caps are
  engineering practice, not protocol.

Jitter is the part people leave out. If a thousand clients all receive Retry-After: 120 at the same moment, obeying it perfectly means a thousand requests arrive in the same second, two minutes later - which is how a server that was recovering gets knocked over by the clients that were being polite.

6.7 Which Codes a Cache Will Store Without Being Asked

This surprises almost everyone. RFC 9110 lists the status codes that are heuristically cacheable, meaning a cache may store and reuse them even when you sent no caching headers at all:

200  203  204  206  300  301  308  404  405  410  414  501

everything else is NOT heuristically cacheable
       ... including 302, 307, 429 and every 5xx

Read that list twice. 404, 405, 410, 414 and 501 are in it. A 404 served with no cache headers may be remembered by a proxy or a CDN, which is why a page can keep returning "not found" for some visitors minutes after you published it. If your 404 handler is dynamic, say so explicitly with Cache-Control: no-store.

When you suspect that is what happened, the response says so. RFC 9111 defines Age as a cache's estimate of how many seconds ago the response was generated, and states the consequence plainly: "the presence of an Age header field implies that the response was not generated or validated by the origin server for this request".

$ curl -sI https://www.iana.org/ | grep -Ei '^HTTP/|^cache-control|^age:'
HTTP/2 200
cache-control: public, max-age=3600
age: 3023        ── a cache answered. This copy was made 3023 seconds ago,
                    so it has about 10 minutes of freshness left

That single header separates "my server is still wrong" from "my server is fine and something in front of it is still holding the old answer". Check it before you change anything, because the second case is fixed by waiting or purging, not by editing.

The flip side is just as useful: 302 and 307 are not on the list, which is the real difference between them and 301/308, and every 5xx is excluded, so a cache will not enshrine your outage.

6.8 The Codes That Are Not HTTP

Some numbers you will meet in logs and error pages are not in the standard at all. They are vendor extensions, and knowing which is which saves you searching the RFCs for something that was never there.

CodeSourceMeaning
444nginxClose the connection with no response at all. It never travels on the wire; it only appears in your log. nginx's own documentation calls it "the non-standard code 444"
499nginxThe client closed the connection before the answer was ready. Also log-only. A rash of these means your pages are too slow, not that anything failed
520 to 530CloudflareEdge-generated errors about the origin: unknown error, origin down, connection timed out, origin unreachable, a timeout, SSL handshake failure, invalid certificate. These do travel on the wire

The distinction matters when you are debugging: a Cloudflare 52x means the edge could not get a usable answer from your server, so nothing in your application log will explain it. Look at the connection, the certificate and the firewall instead.

Do not invent codes of your own. RFC 959 gave that advice for FTP in 1985 - implementations "should not invent new codes for situations that are only slightly different from the ones described here" - and it has aged well. A client that meets your 499 will treat it as a 400, and you will have gained nothing but a number nobody can look up.

6.9 The Complete Register

Every status code lives in one place: the IANA HTTP Status Code Registry. RFC 9110 requires a registration to carry the code, a short description and a pointer to specification text, and adding one needs IETF Review. That is why the list grows by roughly one code every two years rather than one a week.

Below is the whole register as it stood on 7 September 2026: 64 named code points out of the 500 the range allows. Four of them cannot really be used - 305 is deprecated, 306 and 418 are reserved, 510 is obsoleted - and one, 104, holds a temporary registration that expires in November 2026. The rest of the range is genuinely empty, which is why a number you cannot find here is a vendor invention.

CodeNameDefined inIn practice
100ContinueRFC 9110 15.2.1Interim. The answer to a client that sent Expect: 100-continue
101Switching ProtocolsRFC 9110 15.2.2Interim. The second half of the WebSocket handshake
102ProcessingRFC 2518WebDAV. Superseded in practice by 103
103Early HintsRFC 8297Interim. Preload hints sent while the page is still being built
104Upload Resumption SupportedHTTP working group draftTemporary registration for resumable uploads, expiring 13 November 2026
105-199Unassigned
200OKRFC 9110 15.3.1The one you want
201CreatedRFC 9110 15.3.2Something was created. Name it in Location
202AcceptedRFC 9110 15.3.3Queued. HTTP cannot report the outcome later
203Non-Authoritative InformationRFC 9110 15.3.4A proxy modified the body on the way
204No ContentRFC 9110 15.3.5Success, and nothing to send. Cannot carry a body
205Reset ContentRFC 9110 15.3.6Clear the form and stay on the page. Rare
206Partial ContentRFC 9110 15.3.7Answer to a Range request. Video seeking, resumed downloads
207Multi-StatusRFC 4918WebDAV. An XML body carrying a code per resource
208Already ReportedRFC 5842WebDAV. This branch was already reported in this response
209-225Unassigned
226IM UsedRFC 3229Delta encoding. Effectively never deployed
227-299Unassigned
300Multiple ChoicesRFC 9110 15.4.1Several representations, pick one. Almost never used
301Moved PermanentlyRFC 9110 15.4.2Permanent move. Cacheable, and may turn POST into GET
302FoundRFC 9110 15.4.3Temporary move. May turn POST into GET
303See OtherRFC 9110 15.4.4Go and GET this other thing instead. The redirect after a form post
304Not ModifiedRFC 9110 15.4.5Your cached copy is still good. No body
305Use ProxyRFC 9110 15.4.6Deprecated
306(Unused)RFC 9110 15.4.7Reserved. Defined in an earlier version of the specification, no longer used
307Temporary RedirectRFC 9110 15.4.8Temporary move, method preserved
308Permanent RedirectRFC 9110 15.4.9Permanent move, method preserved. The safe replacement for 301 on anything postable
309-399Unassigned
400Bad RequestRFC 9110 15.5.1I could not parse this
401UnauthorizedRFC 9110 15.5.2Not authenticated. MUST carry WWW-Authenticate
402Payment RequiredRFC 9110 15.5.3Reserved for future use since 1992
403ForbiddenRFC 9110 15.5.4Authenticated or not, refused
404Not FoundRFC 9110 15.5.5Nothing here, permanence unknown. Heuristically cacheable
405Method Not AllowedRFC 9110 15.5.6Wrong method for this resource. MUST carry Allow
406Not AcceptableRFC 9110 15.5.7Nothing here matches your Accept headers
407Proxy Authentication RequiredRFC 9110 15.5.8Like 401, but the proxy is asking
408Request TimeoutRFC 9110 15.5.9The request itself never finished arriving
409ConflictRFC 9110 15.5.10Clashes with the current state of the resource
410GoneRFC 9110 15.5.11Deliberately and permanently gone
411Length RequiredRFC 9110 15.5.12Send a Content-Length
412Precondition FailedRFC 9110 15.5.13Your If-Match condition was false. The write was refused
413Content Too LargeRFC 9110 15.5.14Body too large. Usually an upload limit
414URI Too LongRFC 9110 15.5.15The URL is too long. Usually a redirect loop appending parameters
415Unsupported Media TypeRFC 9110 15.5.16Wrong Content-Type for this endpoint
416Range Not SatisfiableRFC 9110 15.5.17The byte range you asked for does not exist
417Expectation FailedRFC 9110 15.5.18Your Expect header cannot be met
418(Unused)RFC 9110 15.5.19Reserved forever because of an April Fools RFC
419-420Unassigned
421Misdirected RequestRFC 9110 15.5.20Wrong server for this hostname. Open a new connection
422Unprocessable ContentRFC 9110 15.5.21Understood, but the content is invalid. The right code for validation errors
423LockedRFC 4918WebDAV. The resource is locked
424Failed DependencyRFC 4918WebDAV. A previous request in the set failed
425Too EarlyRFC 8470Refusing TLS 1.3 early data because it could be a replay
426Upgrade RequiredRFC 9110 15.5.22Come back speaking a different protocol, usually TLS
427Unassigned
428Precondition RequiredRFC 6585Send this write conditionally, so we do not lose an update
429Too Many RequestsRFC 6585Rate limited. Send Retry-After. Never cached
430Unassigned
431Request Header Fields Too LargeRFC 6585Your headers are too big. Usually an enormous cookie
432-450Unassigned
451Unavailable For Legal ReasonsRFC 7725Blocked by a legal demand. Named after Fahrenheit 451
452-499Unassigned
500Internal Server ErrorRFC 9110 15.6.1The application ran and failed
501Not ImplementedRFC 9110 15.6.2This method is not implemented at all
502Bad GatewayRFC 9110 15.6.3A gateway got an invalid answer from upstream
503Service UnavailableRFC 9110 15.6.4Temporarily unavailable. The right code for maintenance
504Gateway TimeoutRFC 9110 15.6.5A gateway waited for upstream and gave up
505HTTP Version Not SupportedRFC 9110 15.6.6That HTTP version is not supported
506Variant Also NegotiatesRFC 2295Content negotiation configured in a circle
507Insufficient StorageRFC 4918WebDAV. Out of storage
508Loop DetectedRFC 5842WebDAV. An infinite loop in the resource tree
509Unassigned
510Not Extended (OBSOLETED)RFC 2774, obsoletedObsoleted
511Network Authentication RequiredRFC 6585Captive portal. Log in to the network, not to the site
512-599Unassigned

The shape of that table is the story of the web. There are 29 client-error codes and 11 server-error codes, because most of the ways an HTTP conversation can fail are ways the request can be wrong. And there are nine redirects for what is essentially one idea, because the first two were specified before anyone knew what browsers would actually do with them.

Back to top

7. Something Most Users Do Not Know

7.1 One Code Is Reserved Forever Because of a Joke

On 1 April 1998 the IETF published RFC 2324, the Hyper Text Coffee Pot Control Protocol, which specified that "any attempt to brew coffee with a teapot should result in the error code 418 I'm a teapot. The resulting entity body MAY be short and stout."

It was a parody. It is also now permanent. So many frameworks implemented 418 as an easter egg that the code became unusable for anything serious, and RFC 9110 formally reserves it: the joke "has been deployed as a joke often enough for the code to be unusable for any future use", so 418 is reserved in the IANA registry and "cannot be assigned to other applications currently". A number in the standard is permanently blocked by a gag from 1998.

7.2 Most Sites Never Look at the Request Method

A server is supposed to answer 405 Method Not Allowed when a method is known but not supported for that resource. Here is what six sites actually did with an unauthenticated DELETE to their home page on 7 September 2026:

$ curl -s -o /dev/null -w '%{http_code}\n' -X DELETE https://example.com/
405
$ curl -s -o /dev/null -w '%{http_code}\n' -X DELETE https://www.iana.org/
405
$ curl -s -o /dev/null -w '%{http_code}\n' -X DELETE https://developer.mozilla.org/en-US/
405
$ curl -s -o /dev/null -w '%{http_code}\n' -X DELETE https://wordpress.org/
405
$ curl -s -o /dev/null -w '%{http_code}\n' -X DELETE https://petermartin.nl/en/
200          ── a full page, rendered, in answer to DELETE

Nothing was deleted, of course. The 200 means something more mundane and more interesting: the application never looked at the method at all. On a typical setup where the web server hands every request to one front controller, the framework reads the path, builds the page and returns it, whatever verb was on the request line. Static and edge-served sites answer 405 because the web server checks the method before any application runs.

This is usually harmless and occasionally not. Anything that treats a method as meaningful - a cache keyed only on the URL, a middlebox that assumes GET is the only safe verb, a security rule that filters on POST - is reasoning about a distinction your application is not making. If you write endpoints that act on data, check the method explicitly rather than assuming the router did it. The methods themselves, and the two properties that decide what a machine is allowed to do with them, are covered in the article on HTTP methods.

7.3 Two of HTTP's Requirements Are Widely Ignored

RFC 9110 uses "MUST" sparingly, and two of them concern status codes. A 401 "MUST send a WWW-Authenticate header field", and a 405 "MUST generate an Allow header field... containing a list of the target resource's currently supported methods". Both requirements exist so that a client knows what to do next instead of guessing.

Both are commonly skipped, including by very large systems. Checked on 7 September 2026:

$ curl -sI https://api.github.com/user | grep -Ei '^HTTP/|^www-authenticate'
HTTP/2 401
             ── no WWW-Authenticate header at all

$ curl -s -o /dev/null -D- -X DELETE https://example.com/ | grep -Ei '^HTTP/|^allow'
HTTP/2 405
             ── no Allow header either

$ curl -s -o /dev/null -D- -X DELETE https://developer.mozilla.org/en-US/ | grep -Ei '^HTTP/|^allow'
HTTP/2 405
allow: GET   ── this one does it properly

The lesson is not that everyone is wrong. It is that you cannot rely on a response being complete just because the code is correct. Write clients that cope with a 401 carrying no challenge, and if you run an API, send the headers - they cost nothing and they are the difference between a client that recovers and a client that retries blindly.

7.4 A 503 on robots.txt Can Stop Crawling of Your Entire Site

This is the most expensive status code mistake available to a website, and it is invisible on every page a human looks at.

Google's robots.txt specification (checked 7 September 2026) documents what happens when the robots.txt file itself returns an error, and the three classes behave completely differently:

robots.txt returnsWhat Google does
2xxUses the rules as provided
3xxFollows at least five hops, then treats the file as a 404
4xx (except 429)Behaves as if no robots.txt exists, and assumes there are no crawl restrictions
5xxStops crawling the site for the first 12 hours. Then uses the last good version for up to 30 days while retrying. After 30 days, either behaves as if there is no robots.txt, or - if the site has general availability problems - stops crawling the site

Both ends of that table are traps. A 5xx on one small text file halts crawling of everything, so a maintenance mode that returns 503 for every URL including /robots.txt is a much bigger event than a maintenance mode that lets robots.txt through. And a 4xx is the opposite failure: your carefully written crawl rules silently stop applying, and everything you meant to exclude becomes fair game.

When you put a site into maintenance, exempt /robots.txt. When you check a site's health, check that one file separately from the rest.

7.5 429 Is a Client Error That Google Counts as a Server Error

429 sits in the 4xx block, so by the letter of the specification it is the client's fault. Google's crawler documentation disagrees in practice: its crawlers "treat the 429 status code as a signal that the server is overloaded, and it's considered a server error". Everywhere else in that document, 4xx means "drop the URL from the index" and 5xx means "slow down and come back". 429 is the single exception, grouped with the 5xx codes.

That makes 429 the correct tool for a specific job: telling a crawler to come back later without telling it your pages are gone. Blocking a crawler with 403 does the opposite, and Google's documentation says so directly.

7.6 A Redirect Can Carry Retry-After

Almost everyone associates Retry-After with 503 and 429. RFC 9110 defines a second use that is barely deployed: "when sent with any 3xx (Redirection) response, Retry-After indicates the minimum time that the user agent is asked to wait before issuing the redirected request".

It is a useful thing to know exists when you are moving a busy endpoint: the redirect tells the client where to go, and the header spreads the arrival over time instead of pointing the entire audience at the new URL in the same second.

7.7 The Worst Failures Have No Status Code at All

Every status code in this article requires one thing to have already worked: a connection to a server that answered. When that does not happen, there is no code to read.

domain expired            no code. DNS answers NXDOMAIN, and nothing is contacted
name server broken        no code. The address is never resolved
port closed / firewalled  no code. "Connection refused" is TCP, not HTTP
certificate expired       no code. The TLS handshake fails before any request
server overloaded         no code. The connection times out

This is why a monitoring setup that only records status codes reports "no errors" during the worst outage a site can have. It is also why the diagnostic order matters: name, then connection, then certificate, then status code. Only the last step produces the numbers in this article. The stages before it are covered in what actually happens when you enter a URL and, for the naming half, in what DNS is and what every record type does.

7.8 Knowing Where Status Codes Stop

A status code makes exactly one claim, and people routinely read three more into it.

  • It does not say the content is correct. A 200 means the server produced a response it considers successful. A page rendering an empty search result, a stack trace, or the words "database connection failed" can all be 200.
  • It does not say the page is indexable. A 200 is necessary but not sufficient. Google's documentation is explicit: "an HTTP 2xx (success) status code doesn't guarantee indexing". A noindex tag, a canonical pointing elsewhere or a robots.txt rule all sit on top of it.
  • It does not say the request was safe. As section 7.2 showed, many applications return 200 for methods they never examined.
  • It is not the last word on where a page lives. A redirect is a strong hint about the canonical address, but canonical tags, sitemaps and internal links all contribute. The status code is one signal among several.

Where status codes stop, other mechanisms take over: response headers for policy, the HTML for meaning, and the crawler's own reports for what a search engine did with all of it. For headers specifically, the neighbouring subject is covered in the article on HTTP headers, which covers the same ground from inside a Joomla install.

Back to top

8. Best Practices

  • Make the status code and the page agree. If the page says "not found", the status line must say 404. Every soft 404 on a site is a page that lies to machines and confuses nobody else.
  • Use 308 and 307 for anything that can be posted to, and 301 or 302 only for pages that are always fetched. The method-rewriting behaviour of 301 and 302 is permitted by the standard, so it is not a bug you can report.
  • Test with 302, publish with 301. A permanent redirect is cached by browsers by default and is genuinely hard to take back.
  • Redirect once, to the final URL. Before adding a rule, check whether an existing rule already points at the URL you are about to leave. Chains cost round trips and rot silently.
  • Return 503 with Retry-After for maintenance, and exempt /robots.txt from it. A 200 maintenance page gets indexed; a 503 on robots.txt stops crawling of the whole site.
  • Send the headers the code requires. 401 needs WWW-Authenticate, 405 needs Allow, 429 and 503 want Retry-After, 201 wants Location. The code alone tells a client what happened; the header tells it what to do.
  • Set Cache-Control explicitly on error pages. 404, 405, 410, 414 and 501 are heuristically cacheable, so silence is a decision you did not know you were making.
  • Rate limit with 429, never with 403. One asks the caller to slow down; the other tells search engines the content is off limits.
  • Do not let your codes enumerate your data. Answering 404 for what does not exist and 403 for what exists but is private tells an outsider which is which. Where that matters, return the same code for both.
  • Retry on the codes that invite it, and never blindly on 502 or 504. Obey Retry-After where it is sent, back off with jitter where it is not, cap the attempts, and never retry a failed retry.
  • Distinguish 400 from 422 in APIs. Unparseable is not the same as invalid, and clients can only act sensibly on the difference.
  • Never branch on the reason phrase. It is advisory in HTTP/1.1 and absent in HTTP/2 and HTTP/3.
  • Check codes after every deploy and every migration, in bulk, from outside the network. A three-line loop over a URL list catches more real problems than any dashboard.
  • Monitor for the absence of a code too. DNS failures, refused connections and expired certificates produce no status code at all, and they are the outages that take a whole site down.
  • Read the primary sources. RFC 9110 section 15 defines every code in one place and is far more readable than its reputation; the IANA registry is the only complete list and tells you which document defines each code. For what search engines do with them, Google's own page on how status codes affect its crawlers is short, current, and more reliable than any summary of it.
Back to top

9. Common Mistakes

9.1 Myth Versus Reality

MythReality
"A 301 and a 302 do the same thing, one is just permanent"They differ on three things that matter: whether a cache may remember the redirect, whether a search engine treats the target as canonical, and, in practice, nothing about method - both may turn a POST into a GET
"410 removes a page from Google faster than 404"Google's current documentation says all 4xx except 429 are treated the same. Use 410 because it is true, not because it is faster
"My error page works, I checked it in the browser"The browser shows you the page, not the code. A soft 404 looks identical to a real one until you run curl -I
"401 means forbidden"401 means unauthenticated - try again with credentials. 403 is the one where credentials will not help
"A 200 means the page is indexed"Google states plainly that a 2xx does not guarantee indexing. It is the entry ticket, not the result
"Error responses are never cached"404, 405, 410, 414 and 501 are heuristically cacheable. A 404 with no cache headers can outlive the problem that caused it
"Blocking a bot with 403 reduces crawl load"It removes your pages from the index and has no effect on crawl rate. 429 is the code that means "slow down"
"500, 502 and 504 all mean the server is broken"They point at three different machines: the application failed, the gateway got no valid answer, the gateway timed out waiting
"An unknown status code will break old clients"The standard requires clients to fall back to the x00 code of the class. A client from 2005 meeting a 451 treats it as a 400
"Uptime monitoring covers this"Only if it checks more than the final status code after redirects, and only if it also reports failures that produce no code at all

9.2 Other Traps to Avoid

  • Redirecting everything to the home page. A deleted page is not the home page. Redirect to the closest equivalent, or return 404. Mass redirects to the root are treated as soft 404s anyway, and they waste the visitor's time.
  • Testing only the final code. %{http_code} after -L hides the entire chain. Check %{num_redirects} too, or print every hop.
  • Redirecting through http://. The unencrypted hop is the one that can be intercepted, and it is usually the one people forget is there.
  • Assuming HEAD tells you what GET would. Most of the time it does. When the answer is surprising, verify with a real GET.
  • Letting a CDN or WAF answer for you without knowing which codes it emits. Cloudflare's 52x range never reaches your application logs, so an outage can be entirely invisible from inside the server.
  • Treating a 504 as proof that nothing happened. The gateway gave up waiting; upstream may have finished the work anyway. Retrying a payment or an order on a timeout is how a customer gets charged twice.
  • Using 200 with an error payload in an API, so that every client has to parse the body to find out whether the call worked. This is the soft 404 problem wearing a JSON hat.
  • Leaving redirects in place forever without review. Each one is a rule that outlives the person who added it, and chains form when nobody checks the existing rules first.
  • Serving a 404 for a page that was never meant to be public. That is fine and deliberate when you are hiding a resource's existence, and a bug when the page really should have been there. Know which one you are doing.
Back to top

10. Summary

A status code is three digits that tell the other machine what to do. Almost every expensive mistake with them comes from treating them as a description of the page instead.

  • The first digit is the class, and it is the only part a client is required to understand. An unrecognised code is handled as the x00 of its class, which is why new codes can be deployed safely. Valid codes run from 100 to 599; anything outside is treated as a 5xx.
  • The 4xx/5xx split decides who fixes it: the request was wrong, or the server failed a reasonable request. The 1992 draft already admitted the two cannot always be told apart.
  • The reason phrase is decoration. It is advisory in HTTP/1.1 and does not exist in HTTP/2 or HTTP/3, which carry a bare :status. Never match on the text.
  • HTTP/0.9 had no status codes, and the 1991 specification says why that failed: "there is no way to distinguish an error response from a satisfactory response except for the content of the text". Every soft 404 recreates that problem deliberately.
  • The five redirects differ on three things: whether the method survives, whether a cache may store the redirect without being told, and how strong a signal a search engine takes from it. 301 and 308 are cacheable and strong; 302, 303 and 307 are neither.
  • 301 and 302 may turn a POST into a GET, by permission of the standard and by the behaviour of curl and every browser. Use 308 or 307 for anything postable, or watch form data disappear without an error.
  • 404 is "nothing here", 410 is "deliberately gone", and a soft 404 is a 200 that lies. Google treats all 4xx except 429 the same, so 410 is about honesty rather than speed.
  • 401 means unauthenticated, 403 means refused. Retrying with credentials can fix the first and not the second, and a server may answer 404 to hide that a forbidden resource exists at all.
  • 429 is the one 4xx that means "come back later", and Google's crawlers count it as a server error rather than a client error. It is the correct way to shed crawl load; 403 is not.
  • 503 with Retry-After is the only honest answer for planned downtime - and /robots.txt must be exempt from it, because a 5xx on that one file stops crawling of the whole site for 12 hours and starts a 30-day clock.
  • 500, 502 and 504 name three different machines: the application failed, the gateway got no valid answer, the gateway gave up waiting. Only the first leaves a stack trace.
  • Conditional requests are why caching works. An ETag plus If-None-Match turns a download into a 304 with no body - still a round trip, but no content.
  • An API needs more than a code. RFC 9457 Problem Details puts the protocol meaning in the status line and the application detail in an application/problem+json body, so infrastructure can act on the code while the caller reads the reason.
  • Some error codes are cached by default. 404, 405, 410, 414 and 501 are heuristically cacheable; 302, 307, 429 and every 5xx are not.
  • The code says whether to retry; the method says whether it is safe. 408, 421, 425, 429 and 503 invite another attempt, and 502 and 504 hide the real question, because the work may have completed after the gateway stopped waiting.
  • IANA has assigned 64 code points out of 500. Four are unusable - 305 deprecated, 306 and 418 reserved, 510 obsoleted - and 418 is blocked permanently by an April Fools RFC from 1998. Numbers outside the register, such as nginx's 444 and 499 or Cloudflare's 52x, are vendor inventions.
  • The worst failures have no status code. An expired domain, a broken name server, a closed port and an expired certificate all fail before HTTP starts, which is why status-code monitoring alone reports nothing during the largest outages.

The reference worth keeping next to a terminal:

THE CLASSES
  1xx  interim, more is coming      4xx  your request was wrong
  2xx  it worked                    5xx  the server failed
  3xx  go somewhere else            unknown code → treat as x00 of its class

THE FIVE REDIRECTS
              method kept?   cacheable by default?   search signal
  301           no                 YES                 strong
  302           no                 no                  weak
  303           no (always GET)    no                  weak
  307           YES                no                  weak
  308           YES                YES                 strong
  postable URL → 308 or 307.   testing → 302, then switch to 301.

CHOOSING THE ERROR
  gone, might return         404
  gone on purpose, forever   410
  not logged in              401  + WWW-Authenticate
  logged in, still no        403
  too many requests          429  + Retry-After
  down right now             503  + Retry-After   (but NOT on /robots.txt)
  app crashed                500       gateway got nothing valid   502
  gateway timed out          504       validation failed           422

READING A CODE
  curl -sI URL | head -1                     the code, no body
  curl -s -o /dev/null -w '%{http_code}\n'   the code from a real GET
  curl -sIL URL | grep -Ei '^HTTP/|^loc'     every hop in the chain
  curl -s -o /dev/null -L \
    -w 'code=%{http_code} hops=%{num_redirects}\n'    both at once

HEADERS THE CODE REQUIRES
  401 → WWW-Authenticate     405 → Allow        201 → Location
  429 → Retry-After          503 → Retry-After  304 → ETag, Vary, Date

CACHED WITHOUT BEING ASKED  (heuristically cacheable)
  200 203 204 206 300 301 308 404 405 410 414 501
  NOT: 302 307 429 and every 5xx
  Age: header present → a cache answered, not your origin server

RETRYING
  invited        408  421  425  429  503      (429 and 503: obey Retry-After)
  careful        502  504   the work may have completed anyway
  pointless      400 404 405 409 410 414 415 422  until you change the request
  no Retry-After back off 1s 2s 4s 8s, add jitter, cap it
  never          a retry of a failed automatic retry

HOP LIMITS
  browsers 20    curl 50    Googlebot 10    Googlebot on robots.txt 5
  Google Inspection Tools: does not follow redirects at all

NO STATUS CODE AT ALL
  NXDOMAIN, connection refused, TLS handshake failure, timeout
  check name → connection → certificate → only then the code

Verified 7 September 2026 against RFC 9110 (HTTP semantics), RFC 9113 (HTTP/2),
the IANA HTTP Status Code Registry, and Google's crawler documentation.

Status codes are the smallest part of a response and the only part everything else obeys. Once you can read them, most "the site is broken" reports resolve into a single number and a single machine. And when pages quietly disappear from search while every human check says the site is fine, the answer is often one file nobody ever visits: a robots.txt that answered 503 for an afternoon.

Back to top
HTTP Status Codes Explained: 301 vs 302, 404 vs 410
Peter Martin
Peter Martin
Joomla Specialist

Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.