Meta Refresh and JavaScript Redirects
A meta refresh and a JavaScript redirect both send the user somewhere else after the server has already returned a successful response. The status code is 200, the body arrives, and the relocation happens in the client. That is the structural difference from a 301, and everything else follows from it.
Search engines do process both, at least in the instant-redirect case. But they process them as a secondary signal derived from content, not as a transport-layer instruction — so the handling is weaker, slower, and less certain than a status code.
Meta refresh
The tag goes in <head> and takes a delay in seconds plus a target URL:
<meta http-equiv="refresh" content="0; url=https://example.com/new-page/">
A delay of 0 is the only value that is treated as a redirect. Documented behaviour is that an instant meta refresh is interpreted similarly to a permanent redirect for indexing purposes, while a delayed meta refresh is not — the page with the tag is treated as the page, and the target is treated as a separate URL that was linked to.
That distinction is worth sitting with, because delayed refreshes are common and are almost always a mistake. A content="5; url=..." on an old page means:
- The old URL stays indexed, with its “you are being redirected” content as its content — which is thin, and reads as a soft 404.
- The new URL is discovered as a link target, with no signal that it supersedes the old one.
- No consolidation occurs.
The delay exists to let a human read a message. If you need to show a message, you have chosen the wrong mechanism — show the message on the destination.
There is also an accessibility problem: an automatic timed refresh moves a user without their action, which fails a well-established accessibility criterion when the delay is short and unavoidable. Another reason 0 is the only defensible value, since an instant refresh is functionally a navigation rather than an interruption.
JavaScript redirects
window.location.replace("https://example.com/new-page/");
Handling depends on rendering. A crawler that renders the page will execute the script and follow the relocation; one that does not will see a 200 with whatever the HTML contained. As of this writing the major engines do render, so a simple synchronous location.replace on load is generally followed and treated much like a redirect.
The reliability drops as the script gets more conditional:
- Executed on load, unconditionally — usually followed.
- Executed after a
fetchresolves — depends on the fetch succeeding within the rendering budget. - Executed on a timer — the delayed-refresh problem in a different syntax.
- Executed conditionally on
navigator,document.cookie, viewport, or geolocation — the crawler’s environment does not match a user’s, so which branch runs is not predictable. This is the case where behaviour is genuinely unreliable and should not be depended on for anything that matters.
location.replace() is preferable to location.href = for redirects because it does not leave the intermediate URL in the browser’s history, so the back button works as a user expects rather than bouncing them forward again.
Why an HTTP redirect wins whenever it is available
One round trip instead of two. A 301 is a header; the client requests the target immediately. A client-side redirect requires the full page — HTML, and for the JavaScript version the scripts too — to be transferred and processed before relocation begins.
No rendering dependency. A status code is read by every user agent. A JavaScript redirect requires script execution.
Unambiguous semantics. 301 says permanent, 302 says temporary, and the distinction is expressible. A meta refresh has no way to say “temporary” — see 301 versus 302 and what each does to link equity for why that distinction is worth keeping.
It works on non-HTML. A PDF cannot contain a meta tag.
It cannot be blocked by a script blocker, an error elsewhere on the page, or a content security policy.
So the rule is simple: if you control any layer that can emit a status code, emit a status code. The layer options are in where to put a redirect.
When client-side is the only option
Genuine cases exist, and they share a shape: you can publish content but not configure responses.
A hosted platform with no redirect configuration. Some site builders and documentation hosts let you edit page content and nothing else. A meta refresh with a zero delay is then the best available instrument.
A static host with no rules layer. Less common now — most static hosts support a redirects file — but if the only thing you can do is write files, a small HTML file containing a zero-delay refresh at the old path is a working redirect.
A page inside a system you do not own. An intranet page, a legacy CMS with no server access.
In each case the mechanism is a fallback chosen because the correct one is unavailable, and it is worth recording as technical debt rather than as a solution. If a redirect matters enough to create, it matters enough to revisit when the platform changes.
If you must use one, make it minimal and unambiguous:
<!-- the whole file, at the old path -->
<title>Redirecting</title>
<meta http-equiv="refresh" content="0; url=https://example.com/new-page/">
<link rel="canonical" href="https://example.com/new-page/">
<p><a href="https://example.com/new-page/">Continue to the new page</a></p>
Four elements, each doing a job: the refresh relocates, the canonical states the consolidation explicitly since the refresh cannot, and the anchor gives a path forward if neither is honoured. The canonical is the important addition — it is a hint rather than an instruction, per canonical tags are a hint, not a command, but it is the only way to state intent at this layer.
What not to do with either
Do not chain them. A meta refresh to a URL that then issues a 301 is a two-hop redirect where one hop requires rendering. Point at the final destination — the same discipline as redirect chains and how to flatten them, with an extra cost per hop.
Do not use them in a migration. A replatform’s redirect map belongs at the server or edge. A migration that relies on client-side redirects is transferring an entire site’s link history through the least reliable mechanism available, and doing it that way is how a replatform loses its link history.
Do not use a JavaScript redirect to show different destinations to different clients. Whatever the intent, the outcome is that a crawler may see a different destination from a user, and content that differs by client is a category of problem you do not want to be in. If you need locale routing, do it server-side on Accept-Language and make every locale independently reachable by its own URL.
Do not leave one in place indefinitely. They are harder to inventory than server rules, because they live in content rather than configuration. Grep for them periodically:
grep -rl 'http-equiv="refresh"' public/
Any hit is a redirect that no config audit will ever find.