A redirect tells a browser and a crawler that a URL has moved. A 301 signals a permanent move and passes most ranking signal to the destination. A 302 signals a temporary move and passes signal inconsistently.
The mechanics are simple. The problems are in the mapping, the chains, and the failures that nobody tests for until traffic has already dropped, which is the migration risk covered in the migration guide.
301 vs 302 vs 307 vs 308: When Each Is Correct
301 Moved Permanently. The URL has changed for good. Use for migrations, URL restructures, and domain changes. Passes ranking signal to the new URL.
302 Found (Temporary Redirect). The page is at a different URL for now and the original will return. Use for A/B tests, temporary maintenance, and seasonal redirects where the original URL will be reactivated. Google may eventually treat a long running 302 as a 301, but relying on that is not planning.
307 Temporary Redirect. Same semantics as 302 but preserves the HTTP method. Relevant for API endpoints redirecting POST requests, not for page SEO.
308 Permanent Redirect. Same as 301 but preserves the HTTP method. Use where a 301 would be correct and the request method matters, which is unusual for content pages.
Understanding status codes more broadly is covered in the status codes guide. In practice: 301 for everything permanent, 302 for genuinely temporary situations, and if you are not sure whether the move is permanent, make it a 301 and stop thinking about it.
Building a Redirect Map That Does Not Lose Pages
The standard process is exporting a URL list from a crawl, mapping old URLs to new ones in a spreadsheet, and uploading the result. That process misses every URL not in the crawl.
The better source list combines several inputs:
# Build a comprehensive source URL list
# 1. Crawl export (pages the crawler found)
cat crawl_export.csv | csvtool col 1 > urls_crawl.txt
# 2. Sitemap URLs (pages you declared)
curl -s https://oldsite.com/sitemap.xml | \
grep -oP '<loc>\K[^<]+' > urls_sitemap.txt
# 3. Search Console URLs (pages Google has indexed)
# Export from GSC > Pages report > all URLs
cat gsc_pages_export.csv | csvtool col 1 > urls_gsc.txt
# 4. Analytics URLs (pages with actual traffic)
cat ga4_pages_export.csv | csvtool col 1 > urls_analytics.txt
# 5. Backlink URLs (pages with external links pointing at them)
cat ahrefs_backlinks.csv | csvtool col 1 > urls_backlinks.txt
# Combine, deduplicate, sort
cat urls_*.txt | sort -u > all_source_urls.txt
echo "Total unique URLs to map: $(wc -l < all_source_urls.txt)"
The backlink list is the one most teams skip and it is the most expensive to miss, because a page with external links that returns a 404 wastes exactly the authority those links carry.
Automated Matching for Large Maps
Mapping five hundred URLs by hand is tedious but feasible. Mapping fifty thousand is not, and most large migrations produce maps with errors in the manual portion.
import csv
from difflib import SequenceMatcher
def build_redirect_map(old_urls: list, new_urls: list, threshold=0.6):
"""Match old URLs to new by slug similarity. Manual review for low scores."""
def slug(url):
return url.rstrip('/').split('/')[-1].lower()
results = []
new_slugs = {slug(u): u for u in new_urls}
for old in old_urls:
old_slug = slug(old)
# Exact slug match
if old_slug in new_slugs:
results.append((old, new_slugs[old_slug], 1.0, 'exact'))
continue
# Fuzzy match
best_score = 0
best_match = None
for new_slug, new_url in new_slugs.items():
score = SequenceMatcher(None, old_slug, new_slug).ratio()
if score > best_score:
best_score = score
best_match = new_url
if best_score >= threshold:
results.append((old, best_match, best_score, 'fuzzy'))
else:
results.append((old, '# NEEDS MANUAL REVIEW', best_score, 'unmatched'))
return results
# Usage:
# map = build_redirect_map(old_urls, new_urls)
# Write to CSV, review anything marked 'unmatched' or 'fuzzy'
Anything below the similarity threshold gets flagged for manual review rather than being silently mapped to the wrong destination. Automated mapping without review produces confident errors at scale.
Redirect Chains: The Problem That Accumulates
A redirect chain is A to B to C, where a single hop from A directly to C would have worked. Chains accumulate across migrations: the 2022 move created A to B, the 2025 move created B to C, and nobody went back to flatten A.
Google follows chains up to a point and drops the request beyond that. Crawl budget is consumed on each hop. And internal links pointing at old URLs force every crawler through the chain on every visit.
import requests
def detect_chains(urls: list, max_hops=10):
"""Find redirect chains and their length."""
chains = []
for url in urls:
hops = []
current = url
for _ in range(max_hops):
try:
r = requests.head(current, allow_redirects=False, timeout=5)
if r.status_code in (301, 302, 307, 308):
target = r.headers.get('Location', '')
hops.append((r.status_code, current, target))
current = target
else:
break
except requests.RequestException:
hops.append(('error', current, None))
break
if len(hops) > 1:
chains.append({'source': url, 'hops': len(hops), 'chain': hops})
return chains
# chains = detect_chains(all_source_urls)
# Fix: rewrite the first redirect to point directly at the final destination
Run this before every migration, because the existing chain state determines whether your new redirects add one hop or four.
Server Configuration
Where you implement redirects depends on your infrastructure. The important thing is picking one place and being consistent.
# Nginx: permanent redirects
server {
# Individual URL redirect
location = /old-page {
return 301 /new-page;
}
# Pattern based: old blog structure to new
location ~ ^/blog/(\d{4})/(\d{2})/(.+)$ {
return 301 /blog/$3;
}
# Entire domain move
server_name oldsite.com;
return 301 https://newsite.com$request_uri;
}
# Apache .htaccess: permanent redirects
# Individual URL
Redirect 301 /old-page /new-page
# Pattern based with regex
RedirectMatch 301 ^/blog/[0-9]{4}/[0-9]{2}/(.+)$ /blog/$1
# Entire domain in virtualhost
RewriteEngine On
RewriteCond %{HTTP_HOST} ^oldsite\.com$ [NC]
RewriteRule ^(.*)$ https://newsite.com/$1 [R=301,L]
// Next.js: in next.config.js
module.exports = {
async redirects() {
return [
{ source: '/old-page', destination: '/new-page', permanent: true },
{ source: '/blog/:year/:month/:slug', destination: '/blog/:slug', permanent: true },
]
},
}
The configuration approach you pick should match your deployment workflow, with the same consistency required for canonical tags covered in the canonicalization guide.
Framework level redirects are often easiest to manage and deploy alongside code changes. Server level redirects avoid processing by the application entirely. Pick the level that matches your deployment workflow.
Post Migration Verification
After deploying, verify rather than assuming.
# Verify redirect map: check every source URL resolves correctly
while IFS=, read -r old_url new_url; do
final=$(curl -sI -o /dev/null -w '%{url_effective}' -L "$old_url")
if [ "$final" != "$new_url" ]; then
echo "MISMATCH: $old_url -> expected $new_url, got $final"
fi
done < redirect_map.csv
Run this on the full map immediately after deployment. Every mismatch is a page losing signal, and they accumulate into the traffic drops covered in the algorithm updates guide, since a botched migration during an update window makes diagnosis considerably harder.
The Internal Link Problem Nobody Fixes
Deploying redirects handles external traffic and external links. Internal links on your own site still pointing at old URLs force every crawler through a redirect on every visit.
After every migration, run a crawl and fix every internal link pointing at a redirected URL. This is the most commonly skipped post migration step and the one that costs crawl budget indefinitely, per the technical audit guide.
What Redirects Mean for AI Crawlers
Most AI crawlers follow redirects, but their tolerance for chains and their behaviour on temporary versus permanent redirects is less consistent than Google's. The crawlers guide covers the specifics.
The NotioncCue AI Crawler Audit verifies what a specific crawler receives from a URL, including whether a redirect resolved correctly or dropped partway through a chain.
Start your free NotioncCue trial and check a few redirected URLs across crawlers. Inconsistent redirect handling across engines is common and it means some engines know your new URL and some still reference the old one.
Run the chain detection script on your domain before adding any new redirects. Most sites discover existing chains they did not know about, and adding a new redirect on top of an existing chain turns a two hop chain into a three hop one that Google may not follow.
Common Questions
How long should redirects stay in place?
Indefinitely, unless you are certain no external link or bookmark points at the old URL. In practice, permanently, because removing a redirect after a year turns those surviving links into 404s.
Does a redirect lose ranking signal?
A small amount. Google has said a 301 passes close to full value, which implies some loss. The loss from a clean redirect is far less than the loss from a 404 where a redirect should have been.
When should I use a 302 over a 301?
Only when the original URL will genuinely return. A/B tests, scheduled maintenance, and seasonal content are legitimate cases. If you are not sure, use a 301. An incorrect 302 delays signal transfer. An incorrect 301 is trivially correctable by removing it.