HTTP Redirects for SEO: 301, 302, 307, 308 Explained
Master HTTP redirects for SEO. Learn when to use 301, 302, 307, 308 redirects, preserve link equity, and avoid common redirect mistakes.
Introduction
Redirects are one of the most powerful—and misunderstood—tools in SEO. When implemented correctly, they preserve link equity, maintain search rankings, and provide seamless user experiences. When done wrong, they can tank your traffic and confuse search engines.
Whether you're migrating domains, restructuring URLs, or managing outdated content, understanding HTTP redirects is essential for maintaining your SEO performance.
In this comprehensive guide, we'll explore every redirect type, when to use each one, and how to implement them correctly.
What Are HTTP Redirects?
HTTP redirects are server-side instructions that send users and crawlers from one URL to another. They consist of:
- Status code (301, 302, 307, 308, etc.)
- Location header pointing to the new URL
- Optional caching instructions
# HTTP Redirect Response
HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-page
Cache-Control: max-age=3600
# Browser/crawler automatically follows to new URL
When a crawler or browser encounters a redirect, it makes a new request to the Location header URL and treats that as the response.
Redirect Types Explained
301 Moved Permanently
Purpose: Permanent redirect—the original URL has permanently moved to a new location.
SEO Impact:
- ✅ Passes 90-99% of link equity
- ✅ Tells search engines to index the new URL
- ✅ Removes old URL from index over time
# Apache .htaccess
Redirect 301 /old-page https://example.com/new-page
# Or with mod_rewrite
RewriteEngine On
RewriteRule ^old-page$ /new-page [R=301,L]
# Nginx
location = /old-page {
return 301 https://example.com/new-page;
}
# Node.js/Express
app.get('/old-page', (req, res) => {
res.redirect(301, '/new-page');
});
When to use 301:
- Site migration (domain change)
- URL structure change
- Content permanently moved
- Merging duplicate pages
- HTTPS migration
- www to non-www (or vice versa)
302 Found (Temporary Redirect)
Purpose: Temporary redirect—original URL will be used again in the future.
SEO Impact:
- ⚠️ Passes less link equity (50-90%)
- ⚠️ Search engines may keep old URL indexed
- ⚠️ New URL might not rank
# Apache .htaccess
Redirect 302 /temp-page https://example.com/promo-page
# Nginx
location = /temp-page {
return 302 https://example.com/promo-page;
}
# Node.js/Express
app.get('/temp-page', (req, res) => {
res.redirect(302, '/promo-page');
});
When to use 302:
- A/B testing
- Temporary promotions
- Maintenance pages
- Geographic redirects (testing phase)
- Seasonal content
Don't use 302 for permanent changes! Search engines may take months to recognize it should be a 301, wasting link equity.
307 Temporary Redirect (HTTP/1.1)
Purpose: Temporary redirect that preserves the HTTP method (POST, PUT, etc.).
SEO Impact:
- Similar to 302
- More explicit about temporary nature
- Preserves request method
# Apache .htaccess
RewriteEngine On
RewriteRule ^form-page$ /temp-form [R=307,L]
# Nginx
location = /form-page {
return 307 /temp-form;
}
# Node.js/Express
app.post('/form-page', (req, res) => {
res.redirect(307, '/temp-form');
});
When to use 307:
- Temporarily redirecting POST requests
- Form submissions during migration
- API endpoints being moved temporarily
308 Permanent Redirect (HTTP/1.1)
Purpose: Permanent redirect that preserves the HTTP method.
SEO Impact:
- ✅ Same as 301 but preserves method
- ✅ Passes link equity like 301
- ✅ More modern and explicit
# Apache .htaccess
RewriteEngine On
RewriteRule ^api/old$ /api/new [R=308,L]
# Nginx
location = /api/old {
return 308 /api/new;
}
# Node.js/Express
app.post('/api/old', (req, res) => {
res.redirect(308, '/api/new');
});
When to use 308:
- Permanently redirecting POST/PUT/DELETE requests
- API endpoint migrations
- Form actions that changed URLs
Quick Reference Table
Code | Name | Permanence | Method Preserved | Link Equity | Use Case |
---|---|---|---|---|---|
301 | Moved Permanently | Permanent | No (GET) | 90-99% | Site migrations, URL changes |
302 | Found | Temporary | No (GET) | 50-90% | A/B tests, promos |
307 | Temporary Redirect | Temporary | Yes | 50-90% | Temp API changes |
308 | Permanent Redirect | Permanent | Yes | 90-99% | Permanent API changes |
Use 301 for permanent GET redirects and 308 for permanent POST/PUT/DELETE redirects.
Common Redirect Patterns
1. HTTPS Migration
# Apache .htaccess - Force HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
# Nginx - Force HTTPS
server {
listen 80;
server_name example.com;
return 301 https://example.com$request_uri;
}
# Node.js/Express - Force HTTPS
app.use((req, res, next) => {
if (!req.secure && req.get('x-forwarded-proto') !== 'https') {
return res.redirect(301, `https://${req.get('host')}${req.url}`);
}
next();
});
2. WWW to Non-WWW (or vice versa)
# Apache - Remove WWW
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1/$1 [R=301,L]
# Apache - Add WWW
RewriteEngine On
RewriteCond %{HTTP_HOST} ^(?!www.)(.+)$ [NC]
RewriteRule ^(.*)$ https://www.%1/$1 [R=301,L]
# Nginx - Remove WWW
server {
server_name www.example.com;
return 301 https://example.com$request_uri;
}
# Nginx - Add WWW
server {
server_name example.com;
return 301 https://www.example.com$request_uri;
}
3. Trailing Slash Normalization
# Apache - Add trailing slash
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !(.*)/$
RewriteRule ^(.*)$ https://example.com/$1/ [R=301,L]
# Nginx - Add trailing slash
rewrite ^([^.]*[^/])$ $1/ permanent;
# Apache - Remove trailing slash
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)/$ /$1 [R=301,L]
# Nginx - Remove trailing slash
rewrite ^/(.*)/$ /$1 permanent;
Consistency matters more than which option you choose. Pick trailing slash or no trailing slash and enforce it site-wide.
4. Old URL Structure to New
# Apache - Redirect old blog structure
RewriteEngine On
# Old: /blog/2025/01/post-title
# New: /articles/post-title
RewriteRule ^blog/[0-9]{4}/[0-9]{2}/(.+)$ /articles/$1 [R=301,L]
# Nginx
location ~ ^/blog/[0-9]{4}/[0-9]{2}/(.+)$ {
return 301 /articles/$1;
}
# Node.js - Pattern-based redirect
app.get('/blog/:year/:month/:slug', (req, res) => {
res.redirect(301, `/articles/${req.params.slug}`);
});
5. Domain Migration
# Apache - Redirect entire domain
RewriteEngine On
RewriteCond %{HTTP_HOST} ^(www.)?old-domain.com$ [NC]
RewriteRule ^(.*)$ https://new-domain.com/$1 [R=301,L]
# Nginx - Domain redirect
server {
server_name old-domain.com www.old-domain.com;
return 301 https://new-domain.com$request_uri;
}
# Cloudflare Workers - Domain redirect
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
if (url.hostname === 'old-domain.com') {
url.hostname = 'new-domain.com';
return Response.redirect(url.toString(), 301);
}
return fetch(request);
}
Redirect Chains and Loops
Redirect Chains
Problem: Multiple redirects in sequence waste crawl budget and slow down users.
# ❌ Redirect chain (3 hops)
URL A → 301 → URL B → 301 → URL C → 301 → URL D
# Each hop:
- Adds latency (network round-trip)
- Consumes crawl budget
- Dilutes link equity
- Frustrates users
# ✅ Direct redirect
URL A → 301 → URL D
URL B → 301 → URL D
URL C → 301 → URL D
How to fix:
# Apache - Find and fix chains
# Audit your redirects regularly
grep -r "Redirect 301" .htaccess | sort
# Update all old redirects to point to final destination
# Old chain:
Redirect 301 /old-1 /old-2
Redirect 301 /old-2 /new
# Fixed:
Redirect 301 /old-1 /new
Redirect 301 /old-2 /new
Redirect Loops
Problem: URL redirects to itself or creates a circular redirect.
# ❌ Redirect loop
URL A → 301 → URL B → 301 → URL A (infinite loop)
# Or:
URL A → 301 → URL B → 301 → URL C → 301 → URL A
# Browser error: "Too many redirects"
How to prevent:
# Apache - Use conditions to prevent loops
RewriteEngine On
RewriteCond %{ENV:REDIRECT_STATUS} !200
RewriteRule ^old-page$ /new-page [R=301,L]
# Test before deploying
curl -I https://example.com/old-page
# Should show single 301 with Location header
# Automated loop detection
function check_redirect_loop(url, max_hops=10):
visited = set()
current = url
for i in range(max_hops):
if current in visited:
return f"Loop detected at {current}"
visited.add(current)
response = fetch(current, follow_redirects=False)
if response.status in [301, 302, 307, 308]:
current = response.headers['Location']
else:
return f"Chain ends at {current} ({i} hops)"
return f"Chain exceeds {max_hops} hops"
Link Equity and PageRank Flow
How Much Link Equity is Preserved?
// Estimated link equity transfer
301 Redirect: 90-99% preserved
302 Redirect: 50-90% preserved (unclear)
307 Redirect: 50-90% preserved
308 Redirect: 90-99% preserved
// Factors affecting transfer:
- Redirect permanence (301/308 better)
- Time since redirect (longer = more stable)
- Redirect chains (each hop loses ~5-10%)
- Content similarity (more similar = better)
// Example calculation:
Original PageRank: 100 units
After 301: ~95 units
After 301 chain (3 hops): ~80 units
After 302: ~70 units
Google's Gary Illyes confirmed that 301 redirects pass "full PageRank" (within rounding errors), effectively treating them as the same as if the content was always at the new URL.
Redirect Implementation
Server-Level Redirects (Best)
Apache (.htaccess or httpd.conf):
# Simple redirect
Redirect 301 /old-page https://example.com/new-page
# Pattern matching with mod_rewrite
RewriteEngine On
RewriteRule ^products/(.*)$ /shop/$1 [R=301,L]
# Multiple conditions
RewriteEngine On
RewriteCond %{HTTP_HOST} ^old-domain.com$ [NC]
RewriteCond %{REQUEST_URI} ^/products/
RewriteRule ^(.*)$ https://new-domain.com/shop/$1 [R=301,L]
Nginx:
# Simple redirect
location = /old-page {
return 301 https://example.com/new-page;
}
# Pattern matching
location ~ ^/products/(.*)$ {
return 301 /shop/$1;
}
# Regex redirect
location ~ ^/blog/(d{4})/(d{2})/(.+)$ {
return 301 /articles/$3;
}
Application-Level Redirects
Next.js:
// next.config.js
module.exports = {
async redirects() {
return [
{
source: '/old-page',
destination: '/new-page',
permanent: true, // 301
},
{
source: '/temp-page',
destination: '/promo',
permanent: false, // 302
},
{
source: '/blog/:year/:month/:slug',
destination: '/articles/:slug',
permanent: true,
},
];
},
};
Express.js:
// Permanent redirect
app.get('/old-page', (req, res) => {
res.redirect(301, '/new-page');
});
// Temporary redirect
app.get('/temp', (req, res) => {
res.redirect(302, '/promo');
});
// Pattern-based redirect
app.get('/products/:id', (req, res) => {
res.redirect(301, `/shop/${req.params.id}`);
});
// Middleware for bulk redirects
const redirects = {
'/old-1': '/new-1',
'/old-2': '/new-2',
'/old-3': '/new-3'
};
app.use((req, res, next) => {
const newUrl = redirects[req.path];
if (newUrl) {
return res.redirect(301, newUrl);
}
next();
});
JavaScript Redirects (Avoid for SEO)
<!-- ❌ JavaScript redirect (not ideal for SEO) -->
<script>
window.location.href = '/new-page';
// Or
window.location.replace('/new-page');
</script>
<!-- Problems: -->
<!-- - Requires JavaScript execution -->
<!-- - Crawlers may not follow -->
<!-- - No HTTP status code -->
<!-- - Link equity may not transfer -->
<!-- ✅ Use only as last resort with meta redirect fallback -->
<meta http-equiv="refresh" content="0; url=/new-page">
<script>window.location.href = '/new-page';</script>
<noscript>
<p>Redirecting to <a href="/new-page">new page</a>...</p>
</noscript>
Testing Redirects
Manual Testing
# Check redirect status
curl -I https://example.com/old-page
# Follow redirects and show chain
curl -L -I https://example.com/old-page
# Show full redirect chain with details
curl -L -v https://example.com/old-page 2>&1 | grep -E "< HTTP|< Location"
# Test specific user agent
curl -I -A "Googlebot" https://example.com/old-page
# Check redirect timing
time curl -I -L https://example.com/old-page
Automated Testing
// Node.js redirect testing
import fetch from 'node-fetch';
async function testRedirect(url: string) {
const response = await fetch(url, { redirect: 'manual' });
if ([301, 302, 307, 308].includes(response.status)) {
const location = response.headers.get('location');
return {
status: response.status,
location,
type: response.status === 301 || response.status === 308
? 'permanent'
: 'temporary'
};
}
return { status: response.status, redirect: false };
}
// Test multiple URLs
const urls = [
'https://example.com/old-1',
'https://example.com/old-2',
'https://example.com/old-3'
];
for (const url of urls) {
const result = await testRedirect(url);
console.log(`${url} → ${result.status} → ${result.location}`);
}
Migration Checklist
When implementing redirects for a site migration:
# Pre-Migration
✓ Audit all URLs (create complete URL inventory)
✓ Map old URLs to new URLs (1:1 mapping)
✓ Identify redirect patterns
✓ Test redirects in staging
✓ Set up monitoring
✓ Create rollback plan
# Implementation
✓ Implement server-level redirects
✓ Test each redirect type
✓ Check for chains and loops
✓ Verify status codes (301 vs 302)
✓ Test with various user agents
✓ Update internal links
✓ Update sitemap
✓ Submit sitemap to Search Console
# Post-Migration
✓ Monitor crawl errors in Search Console
✓ Check redirect coverage (all old URLs handled)
✓ Monitor rankings and traffic
✓ Fix any broken redirects
✓ Update analytics
✓ Monitor for 404 errors
✓ Keep redirects live for 1+ year
Common Mistakes to Avoid
1. Using 302 Instead of 301
# ❌ Permanent change with temporary redirect
Redirect 302 /old-page /new-page
# ✅ Permanent change with permanent redirect
Redirect 301 /old-page /new-page
2. Redirecting to Homepage
# ❌ Lazy redirect to homepage
Redirect 301 /products/widget-123 /
# ✅ Redirect to relevant page
Redirect 301 /products/widget-123 /shop/widgets/widget-123
3. Removing Redirects Too Soon
# ❌ Removing redirects after 1 month
# Some links may take years to update
# ✅ Keep redirects live for at least:
# - 1 year minimum
# - 2+ years for popular pages
# - Forever for critical pages
4. Not Updating Internal Links
# ❌ Relying on redirects for internal links
<a href="/old-page">Link</a> <!-- 301 → /new-page -->
# ✅ Update internal links directly
<a href="/new-page">Link</a> <!-- Direct link -->
Conclusion
HTTP redirects are essential tools for maintaining SEO during site changes. Understanding when to use each redirect type ensures link equity preservation and smooth user experiences.
Key Takeaways:
- ✅ Use 301 for permanent redirects (most common)
- ✅ Use 302 only for truly temporary changes
- ✅ Use 308 for permanent redirects with POST/PUT
- ✅ Avoid redirect chains (direct to final destination)
- ✅ Test thoroughly before deployment
- ✅ Monitor for loops and errors
- ✅ Keep redirects live for 1+ year
- ✅ Update internal links directly
- ✅ Use server-level redirects when possible
- ❌ Never remove redirects prematurely
Master redirects, and your site migrations will preserve rankings and maintain traffic throughout changes.
Next Steps
- Learn about canonical tags for duplicate content
- Study URL structure best practices
- Explore site migration strategies
- Master htaccess configuration
Related Resources: