Skip to main content
โšก Calmops

HTTP 301 Moved Permanently: How Redirects Work

Introduction

HTTP 301 Moved Permanently is one of the most important status codes in web development. It tells clients that a resource has permanently moved to a new URL. Understanding how browsers and search engines handle 301s is essential for SEO, site migrations, and API design.

What is HTTP 301?

A 301 response means: “The resource you requested has permanently moved to the URL in the Location header. Update your bookmarks and use the new URL from now on.”

HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-path

The key word is permanent โ€” both browsers and search engines treat this as a lasting change.

How Browsers Handle 301: Step by Step

First Visit

  1. Browser requests /old-path
  2. Server responds with 301 and Location: /new-path
  3. Browser automatically follows the redirect and requests /new-path
  4. Server responds with 200 OK and the actual content
  5. Browser caches the redirect mapping (/old-path โ†’ /new-path)
Client                          Server
  |                               |
  |  GET /old-path                |
  |------------------------------>|
  |                               |
  |  301 Location: /new-path      |
  |<------------------------------|
  |                               |
  |  GET /new-path                |
  |------------------------------>|
  |                               |
  |  200 OK + content             |
  |<------------------------------|

Subsequent Visits (Cached Redirect)

  1. Browser requests /old-path
  2. Browser checks its cache โ€” finds the cached 301
  3. Browser skips the server entirely and goes directly to /new-path
  4. Server responds with 200 OK
Client                          Server
  |                               |
  |  GET /old-path                |
  |  [cache hit: โ†’ /new-path]     |
  |                               |
  |  GET /new-path                |
  |------------------------------>|
  |                               |
  |  200 OK + content             |
  |<------------------------------|

This is why 301s are “permanent” โ€” the browser stops asking the server about the old URL.

Caching Behavior

By default, 301 redirects are cached indefinitely by browsers (no expiry). This has important implications:

# If you want the redirect to be re-checked, add Cache-Control
HTTP/1.1 301 Moved Permanently
Location: /new-path
Cache-Control: max-age=3600

Without Cache-Control, once a browser caches a 301, it will use the cached redirect even if you later change or remove it โ€” until the user clears their browser cache.

Practical tip: During development or testing, use 302 (temporary) redirects. Switch to 301 only when you’re sure the redirect is permanent.

Implementing 301 Redirects

Nginx

# Redirect a specific path
location /old-path {
    return 301 /new-path;
}

# Redirect entire domain (www to non-www)
server {
    listen 80;
    server_name www.example.com;
    return 301 https://example.com$request_uri;
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

Apache (.htaccess)

# Redirect a specific page
Redirect 301 /old-page.html /new-page.html

# Redirect with RewriteRule
RewriteEngine On
RewriteRule ^old-path/?$ /new-path [R=301,L]

# Redirect entire domain
RewriteCond %{HTTP_HOST} ^www\.example\.com [NC]
RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]

Caddy

example.com {
    redir /old-path /new-path 301
}

Node.js / Express

app.get('/old-path', (req, res) => {
  res.redirect(301, '/new-path');
});

Python / Flask

from flask import redirect, url_for

@app.route('/old-path')
def old_path():
    return redirect(url_for('new_path'), code=301)

Hugo (static site)

# config.toml
[[redirects]]
  from = "/old-path"
  to   = "/new-path"
  status = 301

301 vs Other Redirect Codes

Code Name Permanent? Method preserved? Use case
301 Moved Permanently Yes No (GET on redirect) Permanent URL change
302 Found No No (GET on redirect) Temporary redirect
303 See Other No Always GET After POST, redirect to result
307 Temporary Redirect No Yes Temporary, keep method
308 Permanent Redirect Yes Yes Permanent, keep method (POST stays POST)

Key difference between 301 and 308: With 301, browsers change POST requests to GET when following the redirect. With 308, the original method is preserved. For API redirects where you want to keep POST/PUT/DELETE, use 308.

SEO Implications

Search engines treat 301 redirects as a signal to transfer “link equity” (ranking power) from the old URL to the new one:

  • 301: Passes ~99% of link equity to the new URL โ€” use for permanent moves
  • 302: Passes little or no link equity โ€” search engine keeps indexing the old URL
  • Redirect chains: Multiple hops (A โ†’ B โ†’ C) dilute link equity and slow page load โ€” consolidate to direct redirects

Site Migration Best Practices

# Good: direct 301 from old to new
/old-blog/post-title โ†’ /blog/post-title  (301)

# Bad: redirect chain
/old-blog/post-title โ†’ /temp-path โ†’ /blog/post-title  (two hops)

When migrating a site:

  1. Map every old URL to its new equivalent
  2. Set up 301 redirects for all mapped URLs
  3. Update internal links to point directly to new URLs
  4. Submit the new sitemap to Google Search Console
  5. Monitor crawl errors for several weeks

Common Pitfalls

Redirect Loops

# Bad: infinite loop
location /a {
    return 301 /b;
}
location /b {
    return 301 /a;
}

Always test redirects with curl -L -v to trace the chain:

curl -L -v https://example.com/old-path 2>&1 | grep -E "< HTTP|Location:"

Forgetting to Redirect Both HTTP and HTTPS

# Redirect HTTP โ†’ HTTPS first, then handle the path redirect
server {
    listen 80;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    location /old-path {
        return 301 /new-path;
    }
}

Caching a Wrong 301

If you accidentally set up a 301 to the wrong destination, users who visited will have it cached. You can’t fix their cache remotely โ€” this is why testing with 302 first is a good practice.

Checking Redirects

# Follow redirects and show headers
curl -L -I https://example.com/old-path

# Show each hop in a redirect chain
curl -v --max-redirs 10 https://example.com/old-path 2>&1 | grep -E "< HTTP|> GET|Location"

# Check with wget
wget --server-response --spider https://example.com/old-path 2>&1 | grep -E "HTTP|Location"

Resources

Comments