A cached 301 can break your site after a redesign
Browsers keep permanent redirects for good. If a new site reverses an old 301, returning visitors land in a redirect loop. Here is how to find it and fix it.
A site goes live with a new structure. The owner opens it, clicks a link, and the browser says the page is not working. Every check from the server side looks perfect: the right status codes, the right headers, the right pages. Open the same address in a private window and it works. The problem is not on the server at all. It is a permanent redirect the browser wrote down months ago and never questioned again.
What actually happens
A 301 means the move is permanent, and browsers take that literally. When a response has no explicit cache headers, the browser is allowed to guess how long to keep it, and for a 301 both Chrome and Safari keep it on disk. The next time the address comes up, the browser does not ask the server. It goes straight to the target it remembers.
That is harmless until the direction changes. The pattern I see most often after a rebuild looks like this:
- The old site answered
/with301 -> /index.html, because the index file was generated that way. - Visitors collected that redirect in their cache.
- The new site is clean, so
/is the real page and/index.htmlis a leftover. The obvious tidy up is301 /index.html -> /. - A returning visitor asks for
/, the cache answers/index.html, the server answers/, the cache answers/index.html, and the loop runs until the browser gives up.
New visitors never see it. Crawlers usually never see it, because they do not carry a year of browsing history. The people most likely to hit it are the owner, the team and the regular readers, which is the worst possible sample.
How to see it
The first instinct is to run curl, and curl will tell you everything is fine:
curl -sI https://example.com/ | head -3
# HTTP/2 200
# content-type: text/html; charset=utf-8
# cache-control: no-cacheCurl starts with an empty cache every time, so it can never reproduce the bug. Use the browser instead, with the network panel open and "disable cache" switched off. A cached redirect shows up as a request served (from disk cache) with a 301 status and no network time.
If you want a reproduction you can run over and over, script it. Serve the old behaviour, visit it once, switch the server to the new behaviour and click through:
// one server, two behaviours, one browser profile
if (mode === 'old' && url === '/') return redirect(301, '/index.html');
if (mode === 'new' && url === '/index.html') return redirect(301, '/');With the old rule visited first, the new rule produces the loop in a normal window every time. That is the test that tells you whether your fix works, and it takes a minute to write.
The fix
Stop reversing the redirect. Serve the old URL as a real page and let the canonical tag carry the search engine side:
# /index.html answers with the page itself, no redirect, no loop
location / {
try_files $uri $uri/ =404;
}<link rel="canonical" href="https://example.com/">Search engines are happy with this. A canonical tag is the intended way to say "these two addresses are the same page, index this one", and it does not require the browser to follow anything.
Then clear the stale entry. Clear-Site-Data: "cache" tells the browser to drop the HTTP cache for the origin, cached redirects included. Send it only on the URL that needs it, so a normal visit never pays for it:
map $request_uri $clear_cache {
"~^/index\.html" '"cache"';
default "";
}
server {
add_header Clear-Site-Data $clear_cache;
}An empty value means nginx does not send the header at all, so / stays untouched while /index.html cleans up after itself once.
Finally, tidy the address bar. The visitor arrived on the old URL through a redirect they cannot see, and there is no reason to leave it on screen:
if (location.pathname.endsWith('/index.html')) {
history.replaceState(null, '', location.pathname.slice(0, -10) + location.search + location.hash);
}How to check it worked
Two commands and one click. The old URL answers with the page and the cleanup header, the clean URL answers without it:
curl -sI https://example.com/index.html | grep -i "HTTP/\|clear-site-data"
# HTTP/2 200
# clear-site-data: "cache"
curl -sI https://example.com/ | grep -ci clear-site-data
# 0Then repeat the scripted reproduction. The first click goes through the old URL and lands on the page, and the second click goes straight to the clean URL because the stale redirect is gone.
What to watch out for
Clear-Site-Dataonly works on a secure origin, and only the values you list. Sending"cache"on every page would throw away fonts and images on every visit, which is a performance bug of your own making.- Firefox support has lagged behind Chrome on this header. Treat it as a cleanup that helps most people, not as the thing that makes the page correct. Serving 200 instead of a reverse redirect is what makes it correct.
- Redirect chains are the same class of problem. If
/agoes to/band/blater goes to/c, a returning visitor can be sent through a path you never tested. Test with a profile that has history, not a fresh one. - If the old redirect went to a path you no longer serve at all, you cannot rely on the browser asking you about it. Keep a real response at that address for at least one release cycle.
The general lesson is smaller than the bug: a permanent redirect is a promise, and browsers keep promises longer than we remember making them. Before you flip the direction of an old redirect, assume some readers are still carrying the old one, and give that address something valid to answer with. When you are not certain a move is permanent, a 302 costs nothing and keeps the decision reversible. The same habit of checking with a browser that has history, not a clean profile, catches a surprising number of "works for me" bugs, and it is the same discipline as measuring before optimising rather than guessing.
Questions and answers
- How long does a browser cache a 301 redirect?
- As long as it likes. A 301 without cache headers is heuristically cacheable, and Chrome and Safari both store it on disk, so it can survive restarts and outlive the site that issued it. The only reliable way to shorten that is to send explicit cache headers with the redirect in the first place.
- Why does the site work for me but not for an old visitor?
- A fresh profile has no redirect in its cache, so it follows whatever the server says today. A returning visitor still has the old permanent redirect on disk and never asks the server about that URL. This is also why an incognito window looks fine while the normal window is broken.
- Does Clear-Site-Data clear everything for my users?
- The "cache" value clears the HTTP cache for that origin, which includes cached redirects, images and fonts. It does not touch cookies or storage unless you ask for those values too. Send it only on the URL that needs it, not on every page.
- Should I use a 302 instead to avoid this?
- For anything you might reverse later, yes. A 302 is not cached by default, so a change of mind costs nothing. Keep 301 for moves you are certain about, such as the canonical host or an http to https upgrade.