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
- Browser requests
/old-path - Server responds with
301andLocation: /new-path - Browser automatically follows the redirect and requests
/new-path - Server responds with
200 OKand the actual content - 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)
- Browser requests
/old-path - Browser checks its cache โ finds the cached 301
- Browser skips the server entirely and goes directly to
/new-path - 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:
- Map every old URL to its new equivalent
- Set up 301 redirects for all mapped URLs
- Update internal links to point directly to new URLs
- Submit the new sitemap to Google Search Console
- 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"
Comments