
HTTP Status Codes Explained: 301 vs 302, 404 vs 410
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".
| Class | Name | What it means | Who fixes it |
|---|---|---|---|
1xx | Informational | Received, still working. An interim answer, followed later by a real one | Nobody. You will rarely see one |
2xx | Successful | The request was received, understood and accepted | Nobody |
3xx | Redirection | Something else has to happen before this request completes | Usually the site owner, when it is the wrong kind |
4xx | Client Error | The request was wrong, or is not allowed | Whoever made the request, or whoever published the broken link |
5xx | Server Error | The request looked fine and the server failed anyway | You, 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 top2. 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 digit | FTP, 1985 | HTTP, today |
|---|---|---|
1yz / 1xx | Positive Preliminary reply | Informational |
2yz / 2xx | Positive Completion reply | Successful |
3yz / 3xx | Positive Intermediate reply | Redirection |
4yz / 4xx | Transient Negative Completion reply | Client Error |
5yz / 5xx | Permanent Negative Completion reply | Server 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.
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 Requiredwas 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 suitableChargeToheader". There was going to be a payment header in HTTP. There never was, and402has now been "reserved for future use" for over thirty years.502and503meant different things. In that draft502was "Service temporarily overloaded" and503was "Gateway timeout". Today502is Bad Gateway,503is Service Unavailable and Gateway Timeout has moved to504. 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
4xxcodes are for cases where the client seems to have erred and5xxwhere the server has, but "it is impossible to distinguish these cases in general, so the difference is only informational".
3.3 The Milestones
| Year | Document | What changed |
|---|---|---|
| 1991 | HTTP as implemented in W3 | No status codes at all. An error is a page that happens to say so |
| 1992 | W3C draft list | The first codes, including a 402 that expected a ChargeTo header |
| May 1996 | RFC 1945, HTTP/1.0 | Exactly 15 codes are defined. 302 is called "Moved Temporarily". No 1xx exists |
| January 1997 | RFC 2068, HTTP/1.1 | 36 codes. The 1xx class arrives, plus 206, 405 to 415 and 504. 302 is still "Moved Temporarily" |
| June 1999 | RFC 2616 | 39 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-2010 | RFC 2295, 2324, 2518, 2774, 3229, 4918, 5842 | Extensions bolt on codes for content negotiation, WebDAV, delta encoding, and one joke |
| April 2012 | RFC 6585 | 428, 429, 431 and 511: rate limiting and captive portals finally get codes |
| June 2014 | RFC 7238, then RFC 7538 (April 2015) | 308 arrives as an experiment and becomes a standard, 18 years after HTTP/1.0 standardised 301 |
| February 2016 | RFC 7725 | 451, for content blocked by a legal demand |
| December 2017 | RFC 8297 | 103 Early Hints, the first 1xx anyone had a real use for since 1997 |
| September 2018 | RFC 8470 | 425 Too Early, invented because TLS 1.3 made replay attacks possible |
| June 2022 | RFC 9110 | Semantics are separated from the wire format. One definition of 404 now serves HTTP/1.1, /2 and /3 |
| 2024 onwards | Resumable uploads draft | 104 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.
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.
| Code | Meaning | What it should mean on your site |
|---|---|---|
200 | OK | Here is the page. The overwhelming majority of your traffic |
301 | Moved Permanently | This address changed for good. Update your links and your bookmarks |
404 | Not Found | Nothing here. Someone linked or typed wrong, or you deleted something |
500 | Internal Server Error | Your 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 of499s or5xxs 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
200page that pulls in a404script 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 top5. 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.
| Code | Name | Method kept? | Cacheable by default? | Google treats it as |
|---|---|---|---|---|
301 | Moved Permanently | No. A client MAY turn POST into GET | Yes | A strong signal for the target |
302 | Found | No. A client MAY turn POST into GET | No | A weak signal for the target |
303 | See Other | No. The client makes a fresh GET, by design | No | A weak signal, same as 302 |
307 | Temporary Redirect | Yes. The client MUST NOT change it | No | A weak signal, same as 302 |
308 | Permanent Redirect | Yes. The client MUST NOT change it | Yes | A 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:
| Client | Limit | Source |
|---|---|---|
| Chrome and Firefox | 20 hops | Chromium's kMaxRedirects and Firefox's network.http.redirection-limit, both following the WHATWG Fetch specification |
curl | 50 hops | The default for --max-redirs |
| Googlebot, normal crawling | 10 hops | Google's crawler documentation |
| Googlebot, fetching robots.txt | at least 5, then it is treated as a 404 | Google's robots.txt specification |
| Google Inspection Tools | 0. It does not follow redirects at all | Google'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 Foundmeans 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 Gonemeans 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 Unauthorized | 403 Forbidden | |
|---|---|---|
| Means | You have not proved who you are | I know who you are, and the answer is still no |
| Required header | WWW-Authenticate - the server MUST send it | None |
| Should the client retry? | Yes, with credentials | Not with the same credentials |
| Typical cause | Missing or expired token, no HTTP auth header | Wrong 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
200with 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
302to a maintenance page moves the problem, and now you have to remember to remove it. - A
404tells 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.
| Code | Who sent it | What it usually means | Where to look |
|---|---|---|---|
500 | Your application | The code ran and threw. A fatal error, an uncaught exception, a bad configuration | The application error log |
502 | A gateway in front of it | The gateway reached the application and got an invalid response, or none. The process died or is not listening | Whether the application process is running, and the socket or port it listens on |
504 | A gateway in front of it | The gateway waited and gave up. The application is alive but too slow | Slow 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.
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:
| Code | When it appears |
|---|---|
412 Precondition Failed | You 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 Required | The 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 Content | The 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 Continueanswers a client that sentExpect: 100-continueand 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 Protocolsis how a connection stops being HTTP. It is the second half of the WebSocket handshake, and the server MUST name the new protocol in anUpgradeheader.103 Early Hints(RFC 8297, December 2017) is the interesting one. The server sendsLinkheaders 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.
| Code | Use it for | Detail that matters |
|---|---|---|
201 Created | A successful POST or PUT that made something | Send a Location header naming the new resource |
202 Accepted | Queued for later processing | Deliberately noncommittal. Point at a status resource, because HTTP has no way to report back later |
204 No Content | Success with nothing to return, such as a delete | It cannot carry a body at all, not even an empty JSON object |
400 Bad Request | Malformed request the server cannot parse | Not the catch-all it is usually used as |
409 Conflict | Valid request that clashes with current state | The body should carry enough to resolve the clash |
422 Unprocessable Content | Well-formed request, semantically wrong content | The right code for validation errors, where 400 is usually used |
429 Too Many Requests | Rate limit reached | Always 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 Requestmeans 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 answers425and the client retries once the handshake is complete.426 Upgrade Requiredrefuses 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 aLinkheader withrel="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 Requiredis 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.
| Code | Retry? | On what terms |
|---|---|---|
408 | Yes | The request never fully arrived. RFC 9110: if the client "has an outstanding request in transit, it MAY repeat that request" |
421 | Yes | Over a fresh connection. The one code that may be retried "whether or not the request method is idempotent" (section 6.5) |
425 | Yes | Required, in fact: clients that use TLS 1.3 early data "MUST retry requests upon receipt of a 425" |
429 | Yes | After Retry-After, and not before |
503 | Yes | After Retry-After. This is the code that means "later", so honour the number |
502, 504 | Carefully | The connection failed, but the work may still have happened. See below |
500 | Rarely | The 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, 422 | No | Nothing about the situation changes by repeating. Retry only after changing the request |
401, 403 | Only once | 401 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.
| Code | Source | Meaning |
|---|---|---|
444 | nginx | Close 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" |
499 | nginx | The 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 530 | Cloudflare | Edge-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.
| Code | Name | Defined in | In practice |
|---|---|---|---|
100 | Continue | RFC 9110 15.2.1 | Interim. The answer to a client that sent Expect: 100-continue |
101 | Switching Protocols | RFC 9110 15.2.2 | Interim. The second half of the WebSocket handshake |
102 | Processing | RFC 2518 | WebDAV. Superseded in practice by 103 |
103 | Early Hints | RFC 8297 | Interim. Preload hints sent while the page is still being built |
104 | Upload Resumption Supported | HTTP working group draft | Temporary registration for resumable uploads, expiring 13 November 2026 |
105-199 | Unassigned | ||
200 | OK | RFC 9110 15.3.1 | The one you want |
201 | Created | RFC 9110 15.3.2 | Something was created. Name it in Location |
202 | Accepted | RFC 9110 15.3.3 | Queued. HTTP cannot report the outcome later |
203 | Non-Authoritative Information | RFC 9110 15.3.4 | A proxy modified the body on the way |
204 | No Content | RFC 9110 15.3.5 | Success, and nothing to send. Cannot carry a body |
205 | Reset Content | RFC 9110 15.3.6 | Clear the form and stay on the page. Rare |
206 | Partial Content | RFC 9110 15.3.7 | Answer to a Range request. Video seeking, resumed downloads |
207 | Multi-Status | RFC 4918 | WebDAV. An XML body carrying a code per resource |
208 | Already Reported | RFC 5842 | WebDAV. This branch was already reported in this response |
209-225 | Unassigned | ||
226 | IM Used | RFC 3229 | Delta encoding. Effectively never deployed |
227-299 | Unassigned | ||
300 | Multiple Choices | RFC 9110 15.4.1 | Several representations, pick one. Almost never used |
301 | Moved Permanently | RFC 9110 15.4.2 | Permanent move. Cacheable, and may turn POST into GET |
302 | Found | RFC 9110 15.4.3 | Temporary move. May turn POST into GET |
303 | See Other | RFC 9110 15.4.4 | Go and GET this other thing instead. The redirect after a form post |
304 | Not Modified | RFC 9110 15.4.5 | Your cached copy is still good. No body |
305 | Use Proxy | RFC 9110 15.4.6 | Deprecated |
306 | (Unused) | RFC 9110 15.4.7 | Reserved. Defined in an earlier version of the specification, no longer used |
307 | Temporary Redirect | RFC 9110 15.4.8 | Temporary move, method preserved |
308 | Permanent Redirect | RFC 9110 15.4.9 | Permanent move, method preserved. The safe replacement for 301 on anything postable |
309-399 | Unassigned | ||
400 | Bad Request | RFC 9110 15.5.1 | I could not parse this |
401 | Unauthorized | RFC 9110 15.5.2 | Not authenticated. MUST carry WWW-Authenticate |
402 | Payment Required | RFC 9110 15.5.3 | Reserved for future use since 1992 |
403 | Forbidden | RFC 9110 15.5.4 | Authenticated or not, refused |
404 | Not Found | RFC 9110 15.5.5 | Nothing here, permanence unknown. Heuristically cacheable |
405 | Method Not Allowed | RFC 9110 15.5.6 | Wrong method for this resource. MUST carry Allow |
406 | Not Acceptable | RFC 9110 15.5.7 | Nothing here matches your Accept headers |
407 | Proxy Authentication Required | RFC 9110 15.5.8 | Like 401, but the proxy is asking |
408 | Request Timeout | RFC 9110 15.5.9 | The request itself never finished arriving |
409 | Conflict | RFC 9110 15.5.10 | Clashes with the current state of the resource |
410 | Gone | RFC 9110 15.5.11 | Deliberately and permanently gone |
411 | Length Required | RFC 9110 15.5.12 | Send a Content-Length |
412 | Precondition Failed | RFC 9110 15.5.13 | Your If-Match condition was false. The write was refused |
413 | Content Too Large | RFC 9110 15.5.14 | Body too large. Usually an upload limit |
414 | URI Too Long | RFC 9110 15.5.15 | The URL is too long. Usually a redirect loop appending parameters |
415 | Unsupported Media Type | RFC 9110 15.5.16 | Wrong Content-Type for this endpoint |
416 | Range Not Satisfiable | RFC 9110 15.5.17 | The byte range you asked for does not exist |
417 | Expectation Failed | RFC 9110 15.5.18 | Your Expect header cannot be met |
418 | (Unused) | RFC 9110 15.5.19 | Reserved forever because of an April Fools RFC |
419-420 | Unassigned | ||
421 | Misdirected Request | RFC 9110 15.5.20 | Wrong server for this hostname. Open a new connection |
422 | Unprocessable Content | RFC 9110 15.5.21 | Understood, but the content is invalid. The right code for validation errors |
423 | Locked | RFC 4918 | WebDAV. The resource is locked |
424 | Failed Dependency | RFC 4918 | WebDAV. A previous request in the set failed |
425 | Too Early | RFC 8470 | Refusing TLS 1.3 early data because it could be a replay |
426 | Upgrade Required | RFC 9110 15.5.22 | Come back speaking a different protocol, usually TLS |
427 | Unassigned | ||
428 | Precondition Required | RFC 6585 | Send this write conditionally, so we do not lose an update |
429 | Too Many Requests | RFC 6585 | Rate limited. Send Retry-After. Never cached |
430 | Unassigned | ||
431 | Request Header Fields Too Large | RFC 6585 | Your headers are too big. Usually an enormous cookie |
432-450 | Unassigned | ||
451 | Unavailable For Legal Reasons | RFC 7725 | Blocked by a legal demand. Named after Fahrenheit 451 |
452-499 | Unassigned | ||
500 | Internal Server Error | RFC 9110 15.6.1 | The application ran and failed |
501 | Not Implemented | RFC 9110 15.6.2 | This method is not implemented at all |
502 | Bad Gateway | RFC 9110 15.6.3 | A gateway got an invalid answer from upstream |
503 | Service Unavailable | RFC 9110 15.6.4 | Temporarily unavailable. The right code for maintenance |
504 | Gateway Timeout | RFC 9110 15.6.5 | A gateway waited for upstream and gave up |
505 | HTTP Version Not Supported | RFC 9110 15.6.6 | That HTTP version is not supported |
506 | Variant Also Negotiates | RFC 2295 | Content negotiation configured in a circle |
507 | Insufficient Storage | RFC 4918 | WebDAV. Out of storage |
508 | Loop Detected | RFC 5842 | WebDAV. An infinite loop in the resource tree |
509 | Unassigned | ||
510 | Not Extended (OBSOLETED) | RFC 2774, obsoleted | Obsoleted |
511 | Network Authentication Required | RFC 6585 | Captive portal. Log in to the network, not to the site |
512-599 | Unassigned | ||
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 top7. 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 returns | What Google does |
|---|---|
2xx | Uses the rules as provided |
3xx | Follows 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 |
5xx | Stops 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
200means 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 be200. - It does not say the page is indexable. A
200is necessary but not sufficient. Google's documentation is explicit: "an HTTP2xx(success) status code doesn't guarantee indexing". Anoindextag, 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
200for 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 top8. Best Practices
- Make the status code and the page agree. If the page says "not found", the status line must say
404. Every soft404on a site is a page that lies to machines and confuses nobody else. - Use
308and307for anything that can be posted to, and301or302only for pages that are always fetched. The method-rewriting behaviour of301and302is permitted by the standard, so it is not a bug you can report. - Test with
302, publish with301. 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
503withRetry-Afterfor maintenance, and exempt/robots.txtfrom it. A200maintenance page gets indexed; a503on robots.txt stops crawling of the whole site. - Send the headers the code requires.
401needsWWW-Authenticate,405needsAllow,429and503wantRetry-After,201wantsLocation. The code alone tells a client what happened; the header tells it what to do. - Set
Cache-Controlexplicitly on error pages.404,405,410,414and501are heuristically cacheable, so silence is a decision you did not know you were making. - Rate limit with
429, never with403. 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
404for what does not exist and403for 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
502or504. ObeyRetry-Afterwhere it is sent, back off with jitter where it is not, cap the attempts, and never retry a failed retry. - Distinguish
400from422in 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.
9. Common Mistakes
9.1 Myth Versus Reality
| Myth | Reality |
|---|---|
"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 soft404s anyway, and they waste the visitor's time. - Testing only the final code.
%{http_code}after-Lhides 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
HEADtells you whatGETwould. Most of the time it does. When the answer is surprising, verify with a realGET. - Letting a CDN or WAF answer for you without knowing which codes it emits. Cloudflare's
52xrange never reaches your application logs, so an outage can be entirely invisible from inside the server. - Treating a
504as 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
200with 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 soft404problem 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
404for 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.
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
x00of its class, which is why new codes can be deployed safely. Valid codes run from 100 to 599; anything outside is treated as a5xx. - The
4xx/5xxsplit 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
404recreates 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.
301and308are cacheable and strong;302,303and307are neither. 301and302may turn aPOSTinto aGET, by permission of the standard and by the behaviour of curl and every browser. Use308or307for anything postable, or watch form data disappear without an error.404is "nothing here",410is "deliberately gone", and a soft404is a200that lies. Google treats all4xxexcept429the same, so410is about honesty rather than speed.401means unauthenticated,403means refused. Retrying with credentials can fix the first and not the second, and a server may answer404to hide that a forbidden resource exists at all.429is the one4xxthat 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;403is not.503withRetry-Afteris the only honest answer for planned downtime - and/robots.txtmust be exempt from it, because a5xxon that one file stops crawling of the whole site for 12 hours and starts a 30-day clock.500,502and504name 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
ETagplusIf-None-Matchturns a download into a304with 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+jsonbody, so infrastructure can act on the code while the caller reads the reason. - Some error codes are cached by default.
404,405,410,414and501are heuristically cacheable;302,307,429and every5xxare not. - The code says whether to retry; the method says whether it is safe.
408,421,425,429and503invite another attempt, and502and504hide 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 -
305deprecated,306and418reserved,510obsoleted - and418is blocked permanently by an April Fools RFC from 1998. Numbers outside the register, such as nginx's444and499or Cloudflare's52x, 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.


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










