301 vs 302: the choice is cheap, the chain is what costs you
The difference fits in two lines. A 301 says this address has moved for good, update your records. A 302 says use this other address for now, keep asking me at the original one.
That is genuinely all there is to the choice, and the industry has spent fifteen years writing about it because it is easy to write about. The mistake that actually damages client sites is somewhere else entirely, and it does not show up in any status check, because each individual response is a healthy redirect.
The chain nobody looks at
Type a bare domain into a browser and count the hops.
curl -sSIL -o /dev/null -w '%{num_redirects} hops, final %{http_code} %{url_effective}\n' http://example.com/page
To see each step rather than the total:
curl -sSIL http://example.com/page | grep -iE '^HTTP/|^location:'
On a site that has been maintained by three agencies over ten years, four hops is normal and I have seen six. The pattern assembles itself one reasonable decision at a time: plain HTTP to HTTPS, then non-www to www, then a trailing slash rule, then a legacy rule from a migration in 2019 that maps the old URL scheme, then a language prefix added when the client opened a second market.
Each rule was correct when it was written. Nobody ever composed them, because nobody ever requested the original address again.
What it costs: a full round trip per hop, including a TLS handshake on the first one, on connections where that matters most, which is to say phones on poor networks. It also multiplies the chance that one link in the chain outlives its target, and a chain that ends in a 404 is invisible to anything that only checks the final URL.
The fix is not to remove rules. It is to make the first rule land on the final destination in one move. Keep every legacy mapping, but have each one point at the canonical form directly rather than handing off to the next rule in line.
When a 302 is the right answer
Folklore says always use 301. That is bad advice given confidently, and it produces its own class of incident, which is worse than the problem it solves because a 301 is remembered by browsers.
| Situation | Code | Why |
|---|---|---|
| Page moved, old URL retired | 301 | The move is permanent and you want the signal transferred |
| Site in maintenance, everything to a notice page | 302, or better 503 | Nothing moved. A 301 here tells the world your homepage is now the maintenance notice |
| Sending visitors to a country or language version | 302 | The right target depends on the visitor, so there is no permanent mapping to state |
| Short link, campaign URL, A/B split | 302 | The destination will change and you need to keep control of it |
| Out-of-stock product page pointing at its category | 302 | It may come back, and a 301 forfeits the original URL |
The maintenance row is where real damage happens. A permanent redirect is cached by the browser, and by default it is cached aggressively and for a long time. Put a 301 on a client's homepage during a two-hour maintenance window and the visitors who came during that window will keep landing on the notice page after the site is back, from their own cache, with no request reaching your server at all. You cannot fix it from your side. You have to wait, or serve a redirect back, which is its own mess.
A 302 avoids that. A 503 with a Retry-After header avoids it and is honest with crawlers on top, which is the argument for treating maintenance as a status rather than a page.
307 and 308, briefly, because they exist for a real reason
A 301 or 302 lets a client change the request method on the way. Historically, browsers turned a redirected POST into a GET, and enough of the web depended on that behaviour that it was written into the specification as permitted.
307 and 308 are the same two meanings, temporary and permanent, with that permission removed: the method and the body are preserved. For ordinary page redirects it makes no difference, since GET stays GET either way. It matters on anything that receives a POST: a form endpoint, a webhook receiver, an API path. Redirect one of those with a 301 and the payload can quietly disappear, leaving you with a 200 and an empty request.
If you are writing a rule that could ever catch a POST, use 308. It costs nothing and removes a failure mode that is close to impossible to diagnose from the far end.
What breaks later, and how you find out
Redirects fail silently in three ways, and none of them shows up as an outage.
The target dies. A redirect to a page that was later deleted returns 301 forever, pointing at a 404. Any check that follows redirects and looks at the final code catches this; a check that looks at the first response never will.
The loop. Two rules that point at each other, usually a server rule and an application rule that disagree about the trailing slash or about www. The browser gives up and shows an error that names no cause. curl reports it plainly if you let it follow.
The rule that does nothing. Written, deployed, never applied, because it sits after a catch-all, or because the server does not support the flag it uses. We have that in production: on o2switch, which runs LiteSpeed, the [G], [F] and [R=410] flags in .htaccess are read without complaint and have no effect. The file parses, the deploy succeeds, and the URLs keep answering 200. We only discovered it by requesting the URLs afterwards.
Which is the general lesson about redirects, and about server configuration in general: the deploy succeeding tells you the file was accepted, not that the rule fires. Every redirect rule you add is worth exactly one curl -sSIL against a real URL it should catch, and one against a URL it should not. Two commands, thirty seconds, and it is the difference between a rule and a hope. The wider question of what a 200 fails to tell you runs through the nine codes worth reading in production.
Defined in RFC 9110, section 15.4.2, which puts it in one sentence: “The 301 (Moved Permanently) status code indicates that the target resource has been assigned a new permanent URI.” Read on 3 August 2026.