The short answer

  • 301 Moved Permanently: the old URL is gone for good. Browsers may remember it, and search engines move their attention to the new URL.
  • 302 Found: the old URL is still the real one; send people elsewhere for now. Nothing is remembered unless you say so, and search engines keep the old URL.

If you’d be unhappy to see the old URL in search results a year from now, it’s a 301. If you need the old URL to work again later (a maintenance page, a campaign, a geo or login redirect), it’s a 302.

The rest of this page is about the details that actually cause trouble: caching, request methods, what Google does with each, and chains.

See what your server actually sends

Don’t trust the config file; read the response. With curl:

curl -s -o /dev/null -D - http://example.com/old-page
HTTP/1.1 301 Moved Permanently
Server: nginx
Location: https://example.com/old-page

-D - prints the response headers, -o /dev/null throws away the body. To follow the whole chain, add -L and filter:

curl -sL -o /dev/null -D - http://example.com/old-page | grep -iE '^(HTTP/|location:)'
HTTP/1.1 301 Moved Permanently
Location: https://example.com/old-page
HTTP/2 301
location: https://www.example.com/old-page
HTTP/2 302
location: https://www.example.com/new-page
HTTP/2 200

That’s three hops and a mix of permanent and temporary. More on why that matters below.

Avoid curl -I for this. It sends HEAD, and some applications answer HEAD differently from GET. In the browser, DevTools → Network with “Preserve log” ticked shows each hop with its status code.

The HTTP check does the same from outside your network: every hop, its status code and Location, and a warning once the chain reaches three redirects.

Caching: why a wrong 301 is hard to take back

This is the part that bites people.

RFC 9110 makes 301 and 308 cacheable by default. If the response carries no Cache-Control or Expires, the browser is free to store the redirect and reuse it, and in practice browsers keep it for a long time. On the next visit the browser doesn’t ask your server at all; it jumps straight to the target.

So when you put up a 301 by mistake, or change your mind, fixing the server fixes it only for people who haven’t visited yet. Everyone who already got the 301 keeps being redirected until their cache entry goes away. You have no way to reach into their browser and remove it.

302 and 307 are the opposite: not cached unless you add explicit caching headers. Change the server, and the next request sees the change.

Practical rules:

  • While you’re still testing a permanent redirect, either use a 302 or give the 301 a short lifetime: Cache-Control: max-age=3600. Switch to a plain 301 once you’re sure.
  • When you’re debugging, your own browser is the least reliable witness. Use curl, or DevTools with “Disable cache” ticked, or a private window.
  • CDNs and reverse proxies can cache redirects too. After changing one, purge the URL at the CDN as well.

For visitors stuck on an old 301: clearing cached browsing data for the site fixes it. For a public site you just have to wait it out, which is why you should get the 301 right before you ship it.

Methods: 301/302 vs 307/308 (and 303)

301 and 302 have an old quirk. The original spec said the client should repeat the request with the same method, but browsers changed POST to GET when following them, and the current spec now allows that. So a form POST to a URL that 301s to another URL arrives there as a GET with no body. curl does the same with -L, unless you pass --post301 or --post302.

The fix came as two new codes:

PermanentTemporary
May change POST to GET301302
Must keep method and body308307

And one more: 303 See Other explicitly means “now GET this other URL”. That’s the right response after a form POST succeeds (the POST/redirect/GET pattern), so a page reload doesn’t resubmit the form.

When to use which:

  • Moving web pages: 301 is fine and universally understood. 308 also works in current browsers and search engines.
  • Moving an API endpoint or anything that receives POST/PUT: 308 for permanent, 307 for temporary. With 301 or 302, clients may silently turn a POST into a GET and lose the body.
  • After a form submission: 303.

You’ll also see a 307 Internal Redirect in Chrome DevTools for http:// to https:// that your server never sent. That’s HSTS: the browser already knows the site is HTTPS-only and upgrades the request itself. DevTools marks it with Non-Authoritative-Reason: HSTS. See the HSTS preload checklist for how that works.

What Google does with each

Google documents 301 and 308 as permanent redirects and 302 and 307 as temporary ones:

  • Permanent: a strong signal that the target URL should be the canonical one. Over time the old URL drops out of the index and the new one takes its place.
  • Temporary: a weak signal. Google generally keeps the source URL as canonical and keeps showing it in results.

Google has also said publicly that redirects themselves don’t lose PageRank, so the old advice that “a 302 leaks link value” isn’t the way to think about it. The real difference is which URL ends up in search results.

Two things to watch:

  • Long-lived 302s. Google has said a 302 that stays in place for a long time may eventually be treated like a permanent redirect. That’s not a feature to rely on. If the move is permanent, say so with a 301 and get the indexing right from the start.
  • Redirecting everything to the homepage. When you remove a section and 301 all its URLs to /, Google may treat those as soft 404s instead of transfers. Redirect each old URL to the closest equivalent page, and let URLs with no equivalent return 404 or 410.

For a domain or URL-structure move, Google recommends keeping the redirects in place for a long time, generally at least a year. Links in old emails, PDFs and other people’s sites keep arriving for much longer, so there’s little reason ever to remove them. The domain migration checklist covers the rest of a move.

The 301 and 302 headers are only one signal. If your rel="canonical", internal links and sitemap still point at the old URL, you’re sending contradictory signals. Point all of them at the final URL.

Where unexpected 302s come from

When people find a 302 where they expected a 301, it’s usually a default they didn’t know about:

WhereDefaultPermanent version
Apache Redirect /old /new302Redirect 301 /old /new (or permanent)
Apache RewriteRule ... [R]302[R=301,L]
nginx rewrite ... redirect302rewrite ... permanent or return 301 ...
PHP header('Location: ...')302header('Location: ...', true, 301)
Express res.redirect(url)302res.redirect(301, url)
Django redirect(...)302redirect(..., permanent=True)
Rails redirect_to302redirect_to ..., status: :moved_permanently
Next.js redirects()permanent: false gives 307permanent: true gives 308

For an HTTP-to-HTTPS redirect in nginx, the usual form is:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

If you use nginx -T to dump the full loaded configuration, grep it for return 30 and rewrite to see every redirect rule in one place, including ones in included files you forgot about.

Redirect chains

A chain is several redirects in a row: http://example.com → https://example.com → https://www.example.com → https://www.example.com/en/. Each hop is a full round trip before the page starts loading, and on mobile networks that adds up.

Limits exist, too. The Fetch standard, which browsers follow, gives up after 20 redirects. Google documents that Googlebot follows up to 10 hops and reports a redirect error after that. Long before either limit, you’re wasting time.

Chains usually grow because different layers each add one rule: the CDN forces HTTPS, the web server adds www, the application adds a language prefix, and an old SEO plugin adds a trailing slash. Fix them by making the first hop go straight to the final URL:

  • Combine HTTPS and host canonicalisation into one rule: http://example.com/page → https://www.example.com/page directly, from whichever layer sees the request first. (Exception: if you plan to submit the domain to the HSTS preload list, http:// must first go to https:// on the same host.)
  • When you retire a redirect target, update the old redirects to point at the new final URL instead of adding another hop.
  • Update internal links so your own pages never link to a URL that redirects.

When the chain comes back to where it started, you have a loop, and the browser shows ERR_TOO_MANY_REDIRECTS. The redirect loop guide covers that case, including the CDN-plus-origin version.

A quick decision list

  • URL changed for good, page requests are GET: 301.
  • URL changed for good, clients POST to it: 308.
  • Temporary detour, original URL comes back: 302 (or 307 if non-GET requests are involved).
  • After processing a form: 303.
  • Not sure yet whether it’s permanent: 302 now, 301 once you’re sure. Moving from 302 to 301 is easy; moving back from a cached 301 isn’t.

Check your own domain now

Free, no sign-up. Runs the exact check this guide describes and shows what to fix.