🍱 Lunchbox Hands

seo

301 vs 302 Redirects: Which to Use, and How to Check What You Actually Send

Permanent vs temporary redirects, where 307 and 308 fit, why redirect chains cost you rankings, and the config snippets for Apache, nginx, Cloudflare, Netlify, and Next.js.

Almost every “301 vs 302” article ends at the same sentence: 301 is permanent, 302 is temporary. True, and useless the moment you’re staring at a live site wondering why a page you migrated six months ago still ranks under the old URL. The interesting part isn’t the definition — it’s what each code does to link equity, what your stack sends by default when you weren’t paying attention, and how to see the truth for a given URL in about three seconds.

The whole 3xx family in one table

CodeMeaningMethod preserved?Passes ranking signalsTypical use
301Moved PermanentlyNo — POST may become GETYes, fullyDomain moves, HTTP→HTTPS, retired URLs
302Found (temporary)No — POST may become GETOriginal URL stays indexedA/B tests, seasonal pages, short outages
303See OtherForced to GETNoPost/Redirect/Get after a form submit
307Temporary RedirectYesOriginal URL stays indexedTemporary API routing; HSTS upgrades
308Permanent RedirectYesYes, fullyAPI endpoint moves where POST must survive
<meta refresh>HTML-leveln/aWeakly, slowlyNothing. Avoid.
JS locationClient-siden/aOnly if the crawler renders itLast resort

Two axes, not one. Permanent vs temporary is 301/308 vs 302/307. Method-preserving or not is 307/308 vs 301/302. RFC 7538 added 308 in 2015 precisely because 301 had a legacy ambiguity: browsers historically turned a redirected POST into a GET, which is fine for a blog post and catastrophic for an API endpoint.

Rule of thumb: for pages, use 301. For API routes where the verb and body must survive, use 308. Don’t agonize over it — 99% of page traffic is GET, and Google treats 301 and 308 identically as permanent signals.

What each one actually does to search rankings

A 301 tells Google the old URL is dead and its accumulated authority belongs to the new one. Rankings generally transfer over a few weeks, not instantly. A 302 says the opposite: keep the old URL indexed, I’ll be back. Google has gotten better at detecting long-lived 302s and treating them as permanent anyway, but “eventually figures it out” is not a migration strategy — you’re volunteering for a period of split or suppressed rankings.

The classic failure is invisible: your framework or host defaults to 302, nobody checks, and a site migration quietly bleeds traffic. Cloud platforms, load balancers, and CMS plugins all differ in what they emit for a “redirect” you configured through a UI.

Two rules worth internalizing:

  • Keep 301s in place for at least a year. Google’s John Mueller has said as much repeatedly. Old backlinks and bookmarks don’t expire on your schedule, and the moment you remove the redirect, that equity goes to a 404.
  • Redirect to the equivalent page, not the homepage. A mass redirect of many deleted URLs to / gets treated as a soft 404 and passes roughly nothing.

Redirect chains are the real tax

A single hop is cheap. A chain is not:

http://example.com/old-page
  → 301 → https://example.com/old-page
  → 301 → https://www.example.com/old-page
  → 301 → https://www.example.com/new-page

Four requests before a byte of HTML arrives — each one a DNS lookup, TLS handshake, and round trip on a phone on cellular. That’s LCP damage on top of the crawl-budget waste. Chains accrete naturally: someone adds HTTPS enforcement, someone else adds a www rule, a third person migrates a URL, and nobody flattens the result.

The fix is boring and effective: collapse every chain to one hop. Point http://example.com/old-page straight at the final destination. It costs one rule change and removes three round trips.

While you’re at it, check for loops (A→B→A, which browsers report as ERR_TOO_MANY_REDIRECTS) and mixed http:// hops inside an otherwise-HTTPS chain — the latter leaks a plaintext request before the upgrade.

The free Redirect Checker follows the full chain for any URL and shows every hop with its status code, so you can see a three-hop chain you thought was one hop. It also flags long chains and mixed HTTP/HTTPS hops directly.

A live example you can verify right now

While researching this post, two documentation URLs behaved differently:

  • discord.com/developers/docs/resources/webhook301docs.discord.com/developers/resources/webhook
  • api.slack.com/messaging/webhooks302docs.slack.dev/messaging/sending-messages-using-incoming-webhooks

Both moved their developer docs to a new host. Discord declared it permanent; Slack, at least at the moment of writing, is still emitting a temporary redirect for a move that looks anything but temporary. That’s the exact scenario that costs a domain its accumulated authority — and it happens at companies with entire platform teams. Which is the point: nobody’s immune, so check.

The 307 you never configured

Open DevTools on an HSTS-enabled site, request http://, and you’ll see a 307 Internal Redirect you definitely didn’t write. That’s the browser upgrading the request to HTTPS before it hits the network, from its HSTS cache. It’s synthetic — there’s no server round trip at all.

Practical consequences:

  • Your server-side “http → https” 301 still matters. It’s what teaches the browser the HSTS rule and what serves every visitor who hasn’t been there before.
  • Testing an HTTP redirect in a browser you’ve already visited the site with will show you the browser’s cache, not your config. Use a fresh profile or a command-line request — curl -IL follows the whole chain and prints each hop’s status with no cache in the way, though it will rewrite POST to GET across a 301/302 exactly like a browser does (see how curl’s -L flag actually behaves).
  • 301s are cached hard by browsers, often indefinitely. Ship a wrong 301 and returning visitors keep following it long after you fix the server. This is the single best argument for using 302 while you’re still experimenting — you can take it back.

Config snippets

Apache (.htaccess):

# Single page, permanent
Redirect 301 /old-page /new-page

# Pattern-based, permanent
RewriteEngine On
RewriteRule ^blog/([0-9]+)/(.*)$ /blog/$2 [R=301,L]

# Force HTTPS + non-www in ONE hop
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]

That last block is the chain-flattening pattern: one rule handles both conditions, so a plain http://www. request lands on the final URL in a single redirect. The htaccess Generator builds these without the escaping mistakes.

nginx:

# Permanent single page
location = /old-page { return 301 /new-page; }

# HTTPS + canonical host in one hop
server {
  listen 80;
  server_name example.com www.example.com;
  return 301 https://example.com$request_uri;
}

Note return 301 vs rewrite ... permanent — the former is cheaper and clearer. nginx’s redirect flag is 302; permanent is 301.

Next.js (next.config.js) — note the inverted default:

module.exports = {
  async redirects() {
    return [
      { source: '/old-page', destination: '/new-page', permanent: true },  // 308
      { source: '/promo', destination: '/sale', permanent: false },        // 307
    ];
  },
};

Next emits 308/307, not 301/302. That’s correct and Google handles it fine, but it surprises people auditing headers.

Netlify (_redirects) — defaults to 301 unless you say otherwise:

/old-page   /new-page   301
/promo      /sale       302
/api/*      https://api.example.com/:splat   200

Cloudflare Rules: the dashboard’s “Dynamic Redirect” makes you pick 301/302/307/308 explicitly. Check what an old Page Rule is set to — the legacy default was 302 in some flows.

A short migration checklist

  1. Map every old URL to its closest equivalent — not the homepage.
  2. Use 301 (or 308 for API routes with non-GET verbs).
  3. Verify each redirect resolves in one hop with a real request, not by reading your config.
  4. Update internal links to point at the new URLs directly. Internal links should never rely on redirects.
  5. Update your sitemap to list only final URLs — generate a fresh one after the move.
  6. Keep the redirects live for at least a year.
  7. Re-crawl and confirm nothing 404s or loops.

Check before you ship

The gap between “the redirect I configured” and “the response my server sends” is where migrations go wrong — a CDN rule, a legacy plugin, or a host-level default silently overrides you. Paste any URL into the Redirect Checker to see the actual chain, hop by hop, with each status code. If a code shows up you don’t recognize, the HTTP Status Code Reference has the full list, and the SEO Analyzer will catch the other on-page issues that tend to travel with a botched migration.