302 (Found / Temporary) and 301 (Moved Permanently) look similar to users but signal very differently to search engines. 301 tells Google to update the indexed URL and transfer link equity to the new location. 302 tells Google to keep the original URL indexed and treat the target as a temporary stand-in. Sites accidentally using 302 for permanent moves bleed ranking signal and confuse Google about which URL is canonical. This guide covers the distinction, when each is correct, and the config changes per platform.
| Status | Meaning | Link equity | Index behaviour |
|---|---|---|---|
| 301 | Moved Permanently | Transferred to target | Target URL gets indexed, source dropped |
| 302 | Found (Temporary) | Mostly stays with source | Source URL stays indexed, target seen as alternate |
| 307 | Temporary (strict method) | Mostly stays with source | Like 302, preserves HTTP method |
| 308 | Permanent (strict method) | Transferred to target | Like 301, preserves HTTP method |
curl -I https://example.com/old-page # Look for: # HTTP/2 301 → permanent (correct for moves) # HTTP/2 302 → temporary (verify intent) # HTTP/2 307 → strict temporary # HTTP/2 308 → strict permanent
# .htaccess # Wrong (default Redirect directive defaults to 302) Redirect /old-page /new-page # Right — explicit 301 Redirect 301 /old-page /new-page # Or with RewriteRule RewriteEngine On RewriteRule ^old-page/?$ /new-page [R=301,L] # WRONG (R alone defaults to 302) RewriteRule ^old-page/?$ /new-page [R,L]
server {
# Wrong: default is 302
rewrite ^/old-page$ /new-page;
# Right: permanent flag → 301
rewrite ^/old-page$ /new-page permanent;
# Or with return (preferred for simple redirects)
location = /old-page {
return 301 /new-page;
}
}
Each redirect rule has a "HTTP code" dropdown. Set to "301 - Moved Permanently" instead of "302 - Found".
Plugin → Redirection → Redirects → edit each rule: HTTP code: 301 Moved Permanently (was 302)
Similar dropdown per redirect. Default to 301 unless temporary.
add_action('template_redirect', function() {
if (is_page('old-slug')) {
wp_redirect(home_url('/new-slug/'), 301); // explicit 301
exit;
}
});
// Wrong: default is 302
app.get('/old-page', (req, res) => {
res.redirect('/new-page');
});
// Right: explicit 301
app.get('/old-page', (req, res) => {
res.redirect(301, '/new-page');
});
from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect
# Wrong: 302
return HttpResponseRedirect('/new-page')
# Right: 301
return HttpResponsePermanentRedirect('/new-page')
# Or in urls.py
from django.views.generic import RedirectView
path('old-page', RedirectView.as_view(url='/new-page', permanent=True))
// next.config.js
module.exports = {
async redirects() {
return [
{
source: '/old-page',
destination: '/new-page',
permanent: true, // → 308 (modern permanent)
},
{
source: '/temp-redirect',
destination: '/somewhere',
permanent: false, // → 307 (modern temporary)
}
];
}
};
Cloudflare → Rules → Bulk Redirects (or Page Rules) For each redirect: Source URL: example.com/old-page Target URL: https://example.com/new-page Status code: 301 (Moved Permanently) ← not 302 Preserve query string: Yes
Many framework redirect helpers default to 302. Always check and explicitly set 301 for permanent moves. Don't assume the framework picked the right code.
A/B test runs for 6 months, winner adopted permanently. 302 left in place. Google still favours the test source URL in search. Once the test concludes, change to 301 for the winning variant.
# Wrong chain /old-page → 302 → /interim → 301 → /new-page # The 302 loses equity that the 301 then partly recovers. # Fix: collapse to one hop /old-page → 301 → /new-page
curl -I https://example.com/old-page | grep -i "^HTTP"Should show HTTP/2 301 (or HTTP/2 308 for modern strict).