Meta refresh redirects: what Google does with them, and what to use instead
A meta refresh is a tag in the page's HTML, <meta http-equiv="refresh" content="5; url=/new-page">, that tells the browser to go to another address or reload the page after a number of seconds. Google follows it, treating an instant one (0 seconds) as a permanent redirect and a delayed one as temporary. But a server-side 301 redirect is clearer for search engines and better for visitors. Use a meta refresh only when you can't set up redirects on the server.
What a meta refresh is
The content holds a delay in seconds and, optionally, an address:
content="0; url=/new" Redirects straight away. Often used on hosting that doesn't allow server redirects.content="5; url=/new" Shows the page for five seconds, then redirects. The classic "this page has moved, you'll be taken there shortly".content="300" No address: reloads the same page every five minutes. Common on old news and live-score pages.A server can send the same instruction as an HTTP header, Refresh: 5; url=/new. It behaves the same way.
How Google treats it
Google lists the ways to redirect in order of how reliably it understands them:
| Method | Google reads it as | Use it |
|---|---|---|
| 301 or 308 from the server | Permanent | Whenever a page has moved for good |
| 302, 303 or 307 from the server | Temporary | When the move really is temporary |
| Meta refresh or Refresh header, 0 seconds | Permanent | Only if you can't redirect on the server |
| Meta refresh or Refresh header, delayed | Temporary | Only if you can't redirect on the server |
| JavaScript redirect | Seen only if the page renders | Only if neither of the above is possible |
The difference matters. A permanent redirect tells Google to replace the old URL with the new one in its results (a strong canonical signal). A delayed meta refresh is read as temporary, so Google may keep showing the old address. A server redirect also answers before any page is sent; a meta refresh tag is only found once the old page has been downloaded and read.
Why it's bad for people
- A delayed redirect moves people without asking. Someone reading, or using a screen reader, is taken somewhere else mid-sentence.
- A timed reload loses their place. The page reloads, so they can lose their place and anything typed into a form.
- A delayed redirect breaks the back button. Going back lands on the page that sends them forward again.
The W3C lists a timed meta refresh redirect (F40) and a meta refresh that reloads the page (F41) as failures of WCAG's timing, interruption and change-on-request rules. An instant (0-second) redirect isn't a failure.
How to check by hand
1Run the snippet 30 seconds
An instant meta refresh moves you on before you can look, so this reads the page's HTML without following it. Open DevTools (F12), go to the Console and paste:
// Finds meta refresh tags (and a Refresh header) and says what each one does. Running it never follows them.
// For a page that redirects instantly, put its address here and run this from another page on the same origin.
const url = location.href;
const parse = content => {
// Same rules as browsers: digits first, then an optional "url=", with or without quotes.
const m = content.match(/^\s*(\d*)(\.[\d.]*)?\s*(?:[;,]\s*|\s+|$)(?:url\s*=\s*)?(['"]?)(.*)$/i);
if (!m || (!m[1] && !m[2])) return null;
let target = m[4].trim();
if (m[3] && target.includes(m[3])) target = target.slice(0, target.indexOf(m[3]));
return { delay: Number(m[1] || 0), target };
};
const describe = (where, content) => {
const p = parse(content);
if (!p) return { where, content, does: 'nothing: browsers ignore this value', google: '' };
if (!p.target) return { where, content, does: 'reloads the page every ' + p.delay + 's', google: '' };
let href;
try { href = new URL(p.target, url).href; } catch { return { where, content, does: 'nothing: the address is invalid', google: '' }; }
return {
where, content,
does: (p.delay === 0 ? 'redirects instantly to ' : 'redirects after ' + p.delay + 's to ') + href,
google: p.delay === 0 ? 'treats it as a permanent redirect' : 'treats it as a temporary redirect',
};
};
const tags = doc => [...doc.querySelectorAll('meta[http-equiv="refresh" i]')]
.map(m => ({ content: m.getAttribute('content') || '', noscript: Boolean(m.closest('noscript')) }));
let res;
try {
res = await fetch(url, { cache: 'no-store' });
} catch {
throw new Error('Could not fetch ' + url + '. Run this from a page on the same origin (same protocol, domain and port).');
}
if (res.redirected) console.log('Note: ' + url + ' redirected on the server to ' + res.url + '. Results are for that page.');
const serverTags = tags(new DOMParser().parseFromString(await res.text(), 'text/html'));
const rows = serverTags.map(t => describe(t.noscript ? 'server HTML, inside <noscript> (only without JavaScript)' : 'server HTML', t.content));
if (url === location.href) {
const fromServer = serverTags.filter(t => !t.noscript).map(t => t.content);
tags(document).filter(t => !fromServer.includes(t.content)).forEach(t => rows.push(describe('added by JavaScript', t.content)));
}
if (res.headers.get('refresh')) rows.push(describe('Refresh header', res.headers.get('refresh')));
console.table(rows.length ? rows : [{ result: 'No meta refresh or Refresh header' }]);
It lists each meta refresh in the server HTML, any added by JavaScript, and a Refresh header if the server sends one, with what each does and how Google treats it. It never follows them. For a page that redirects instantly, open any other page on the same origin, change the address near the top to that page's, and run it there.
It can only fetch pages on the same origin (the same protocol, domain and port) as the page you run it on. For another site, or another subdomain, use method 2.
2View the source without following it 30 seconds
Type view-source: in front of the address in Chrome's address bar, for example view-source:https://example.com/old-page. The source opens without the redirect firing. Search it for refresh.
3Watch the Network tab 1 minute
Open DevTools, tick Preserve log on the Network tab, and load the page. A server redirect shows as a 301 or 302 on the first request. A meta refresh shows as a 200, followed a moment later by a second page loading. A Refresh header also shows as a 200: click the request and look for Refresh under Response Headers.
What to use instead
- Page moved: a 301 redirect on the server, or in your CDN or hosting settings. Most platforms have a redirects page, even ones that don't let you edit server config.
- Page that needs fresh content: update the content with JavaScript, without reloading, and give visitors a way to pause it.
- "This page has moved" notices: redirect straight away on the server. If you want to explain the move, do it on the new page.
- No server access at all: an instant meta refresh (
0seconds) is the next best option, since Google treats it as permanent. Update your internal links to the new address too, so visitors don't hit it at all (see how to find broken links).
Choosing a server redirect instead? See 301 vs 302 redirects.
How crawlers report this
If a crawl sent you here, these are the labels to look for. Our guide to Screaming Frog and Sitebulb issues covers other common ones.
| Screaming Frog | Sitebulb | What it means |
|---|---|---|
| Response Codes: Internal Redirection (Meta Refresh) | Redirects using a Meta refresh | The page redirects through an HTML tag, not a server redirect. |
Doing this on every page?
SEOSnip finds meta refreshes for you
Open the extension on any page and it reads the meta refresh tags in that page. An instant redirect moves you on before you can open the extension, so use the snippet above for those; the check reads the tag, not a Refresh header. You get:
- A flag for a meta refresh that redirects, or one that reloads the page, with the delay and the address
- The tag itself, with its
contenthighlighted - A ticket with that evidence, and a "Check if fixed" button that re-runs the check
Page redirects with a meta refresh
Meta refresh · head > meta:nth-of-type(3)
<meta http-equiv="refresh" content="5; url=https://example.com/coats"> |