HTTP Redirect Status Codes — 301, 302, 307, 308 Explained
HTTP 3xx redirect codes tell clients to look elsewhere for a resource. Here's when to use 301 vs 302 vs 307 vs 308, how redirects affect SEO, and how to implement redirects in...
HTTP 3xx codes tell browsers and crawlers that a resource has moved. The code you choose affects whether the redirect is permanent or temporary, and whether the HTTP method (GET/POST) is preserved.
Use the HTTP Status Codes Reference to look up any HTTP status code.
The 3xx redirect codes
| Code | Name | Permanent? | Method preserved? | Use case |
|---|---|---|---|---|
| 301 | Moved Permanently | Yes | No (GET on follow) | Permanent URL changes |
| 302 | Found | No | No (GET on follow) | Temporary redirects |
| 303 | See Other | No | Always GET | POST-redirect-GET pattern |
| 307 | Temporary Redirect | No | Yes | Temporary, keep POST |
| 308 | Permanent Redirect | Yes | Yes | Permanent, keep POST |
301 Moved Permanently
The resource has permanently moved. Browsers cache it; search engines transfer link equity.
HTTP/1.1 301 Moved Permanently
Location: https://new.example.com/page
Use when:
- Changing your domain (
http://old.com→https://new.com) - Restructuring URLs (
/products/123→/items/123) - Adding HTTPS permanently
- Moving a page forever
SEO effect: Passes PageRank to the new URL. Google typically follows and indexes the new URL within days.
Caution: Browsers cache 301s aggressively. Testing with 301 can be frustrating — use 302 during testing, then change to 301.
302 Found (Temporary)
Temporary redirect — the original URL will come back. Browsers don’t cache it indefinitely.
HTTP/1.1 302 Found
Location: /maintenance
Use when:
- Maintenance pages
- A/B testing
- Conditional redirects based on login state
- Promotional pages that will go away
Note: Both 301 and 302 may change POST to GET on the redirected request (browser behavior varies). Use 307/308 if you need to preserve the method.
303 See Other
Always redirects to a GET request. The canonical use case: after a successful POST form submission, redirect to a success page.
POST /checkout
→ 303 See Other
→ GET /order-confirmation
This is the POST-Redirect-GET pattern. Without it, refreshing the page would resubmit the form.
// Express:
app.post('/checkout', async (req, res) => {
const order = await processOrder(req.body);
res.redirect(303, `/orders/${order.id}/confirmation`);
});
307 Temporary Redirect
Like 302, but guarantees the HTTP method is preserved:
POST /api/v1/users → 307 → POST /api/v2/users
Use when:
- API versioning where you need to preserve POST/PUT/PATCH
- Temporary backend moves while maintaining API compatibility
308 Permanent Redirect
Like 301, but preserves the HTTP method:
POST /api/v1/resource → 308 → POST /api/v2/resource
Use when:
- Permanently moving an API endpoint while maintaining method
Implementing redirects
Express.js
// 301 permanent:
app.get('/old-path', (req, res) => {
res.redirect(301, '/new-path');
});
// 302 temporary (default):
app.get('/promo', (req, res) => {
res.redirect('/current-promotion');
});
// 303 after POST:
app.post('/submit', async (req, res) => {
await saveData(req.body);
res.redirect(303, '/success');
});
// Redirect all HTTP to HTTPS:
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
return res.redirect(301, `https://${req.hostname}${req.url}`);
}
next();
});
Nginx
# HTTP to HTTPS:
server {
listen 80;
return 301 https://$server_name$request_uri;
}
# Old domain to new:
server {
server_name old.example.com;
return 301 https://new.example.com$request_uri;
}
# Specific path redirect:
location /old-path {
return 301 /new-path;
}
# Trailing slash:
rewrite ^(/path)$ $1/ permanent;
Next.js
// next.config.js
module.exports = {
async redirects() {
return [
{
source: '/old-page',
destination: '/new-page',
permanent: true, // 308; false = 307
},
{
source: '/blog/:slug',
destination: '/posts/:slug',
permanent: true,
},
];
},
};
Apache .htaccess
# HTTP to HTTPS:
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
# Old URL to new:
Redirect 301 /old-page.html /new-page
# Redirect entire old domain:
RewriteCond %{HTTP_HOST} ^old\.example\.com [NC]
RewriteRule (.*) https://new.example.com/$1 [R=301,L]
SEO and redirect chains
Avoid redirect chains (A → B → C) — they slow down crawling and dilute PageRank:
# BAD: redirect chain
/page → /old-page → /new-page → /final-page
# GOOD: direct redirect
/page → /final-page
Also avoid redirect loops (A → B → A), which cause browsers to show “Too many redirects” errors.
Related tools
- HTTP Status Codes Reference — look up any status code
- HTTP Status Codes Guide — complete 1xx-5xx reference
- HTTP 404 Not Found — handling missing resources
Related posts
- When to Use 422 vs 400 (and Other HTTP Status Code Debates) — 400 means the request is malformed. 422 means it's valid but semantically wrong.…
- HTTP 301 vs 302 Redirect — Permanent vs Temporary Redirects Explained — Understand when to use 301 (permanent) vs 302 (temporary) redirects, how they af…
- HTTP 404 Not Found — What It Means and How to Fix It — HTTP 404 means the server understood the request but can't find the resource. It…
- HTTP Status Codes Reference — Complete Guide to All Response Codes — HTTP status codes tell clients what happened with their request. Here's a comple…
- REST API Error Handling — HTTP Status Codes and Error Response Design — Design consistent REST API error responses using the right HTTP status codes. Le…
Related tool
Full HTTP status code reference with explanations and when to use each.
Written by Mian Ali Khalid. Part of the Dev Productivity pillar.