Skip to content

308 Permanent Redirect

What is HTTP 308 Permanent Redirect?

308 Permanent Redirect
When something on the internet moves to a new address permanently and never come...
HTTP 308 Permanent Redirect status code illustration

Explain Like I’m 3

The thing you want has moved to a new place forever! And when you go to the new place, you’re going to do the exact same thing you were trying to do before. If you were bringing a drawing, you bring that same drawing to the new place!

Example: Your toy box moved from your room to the playroom forever. When you go to put a toy away, you go to the playroom now and put it in the box the same way you always did!

Explain Like I’m 5

When something on the internet moves to a new address permanently and never comes back, the website can send a ‘308 Permanent Redirect’ message. This tells your browser ‘I’ve moved forever - go to this new address!’ But here’s the important part: your browser does the exact same thing at the new address. If you were submitting a form with your name and email, your browser submits that exact same form with the same information to the new address. It’s different from an older kind of redirect (301) that sometimes changed what you were doing - 308 keeps everything exactly the same, just at a new address. From now on, your browser knows to always use the new address.

Example: You’re submitting your homework to a school website at oldwebsite.com/homework. The school permanently moved to newwebsite.com. The old site responds ‘308 - We’ve permanently moved!’ Your browser automatically submits your homework to newwebsite.com/homework using the exact same ‘submit homework’ action with all your files. From now on, when you type oldwebsite.com, it automatically goes to newwebsite.com.

Jr. Developer

HTTP 308 Permanent Redirect indicates the target resource has been permanently moved to a different URI, and the client MUST use the same HTTP method when redirecting. This is the permanent version of 307, just as 301 is the permanent version of 302. The critical difference: 301 may (and historically did) change POST to GET, while 308 guarantees method preservation. If a client POSTs data to an endpoint that returns 308, it will POST the same data to the new URL. Use 308 for permanent API endpoint migrations where you can’t risk method changes breaking functionality. Introduced in RFC 7538 (2015), now part of RFC 9110. SEO-wise, 308 passes PageRank and link equity like 301, so Google treats them equivalently. Modern browsers and HTTP clients support 308 well. For RESTful APIs, 308 is the semantically correct choice for permanent endpoint moves - it preserves POST, PUT, PATCH, DELETE methods. For traditional web pages (GET requests), 301 works fine since GET→GET doesn’t have method-change concerns, but 308 is clearer about intent.

Example: API v1 permanently deprecated, all endpoints moving to v2. Client POSTs to /api/v1/users with user data. Server returns 308 with Location: /api/v2/users. Client automatically POSTs same data to /api/v2/users. Client library updates cached base URL to v2 for future requests. User created successfully without code changes.

Code Example

// Express.js implementing 308 Permanent Redirect
const express = require('express');
const app = express();
// Permanent API migration - v1 deprecated
app.post('/api/v1/users', (req, res) => {
// V1 permanently moved to V2
// POST method and body preserved
res.redirect(308, '/api/v2/users');
});
app.post('/api/v2/users', async (req, res) => {
// Receives same POST with same body
const user = await db.createUser(req.body);
res.status(201).json(user);
});
// Domain migration with 308
app.use((req, res, next) => {
const oldDomain = 'old-api.example.com';
const newDomain = 'api.example.com';
if (req.headers.host === oldDomain) {
// Permanent redirect to new domain
const newUrl = `https://${newDomain}${req.originalUrl}`;
return res.redirect(308, newUrl);
}
next();
});
// HTTP to HTTPS permanent redirect
app.use((req, res, next) => {
if (req.protocol === 'http') {
// Permanent redirect to HTTPS, preserve method
const httpsUrl = `https://${req.headers.host}${req.originalUrl}`;
return res.redirect(308, httpsUrl);
}
next();
});
const db = {
createUser: async (data) => ({ id: 1, ...data })
};

Crash Course

308 Permanent Redirect, defined in RFC 9110 Section 15.4.9 (originally RFC 7538, 2015), indicates the target resource has been assigned a new permanent URI and future requests SHOULD use one of the enclosed URIs. Critically, per RFC 9110, the user agent MUST NOT change the request method when automatically redirecting - POST stays POST, PUT stays PUT, DELETE stays DELETE. This parallels the relationship between 302 and 307: 301 is ambiguous (historically changed methods), 308 is strict (preserves methods). RFC 7538 introduced 308 specifically to address API migration scenarios where 301’s method-changing behavior broke RESTful semantics. Example: POST /api/resource with JSON body receiving 301 might become GET /api/resource (losing body), breaking the API. 308 guarantees POST /api/resource with body → POST /new/api/resource with same body. SEO implications: Search engines treat 308 identically to 301 - full PageRank transfer, permanent URL replacement in index. Use cases: permanent API endpoint migrations, domain changes for APIs, HTTP→HTTPS upgrades, service consolidation. Caching: 308 is cacheable by default per RFC 9110, and clients should update stored URLs to use redirect target permanently. Browser support: Excellent in modern browsers (Chrome 48+, Firefox 43+, Safari 10.1+, Edge). Legacy client caution: Very old HTTP libraries might not support 308; verify client compatibility for critical APIs.

Example: Company consolidates services: payments.example.com/api permanently moves to api.example.com/payments. Old endpoint: POST payments.example.com/api/charge with {amount: 100, token: ‘xyz’}. Returns 308 with Location: https://api.example.com/payments/charge. Client POSTs to new URL with same body {amount: 100, token: ‘xyz’}. Payment processed. Client library updates base URL permanently. Future requests go directly to api.example.com. If 301 were used, some clients might convert to GET, losing payment data and failing transaction.

Code Example

// Production 308 Permanent Redirect patterns
const express = require('express');
const app = express();
// API version deprecation (permanent)
const DEPRECATED_ENDPOINTS = {
'/api/v1/users': '/api/v2/users',
'/api/v1/orders': '/api/v2/orders',
'/api/v1/products': '/api/v2/products'
};
app.use((req, res, next) => {
const newEndpoint = DEPRECATED_ENDPOINTS[req.path];
if (newEndpoint) {
// Permanent redirect - clients should update URLs
return res.redirect(308, newEndpoint);
}
next();
});
// Subdomain consolidation
app.use((req, res, next) => {
const host = req.headers.host;
// Permanently redirect subdomains to main domain
const subdomainPattern = /^(www|api-v1|legacy)\.(.*)/;
const match = host.match(subdomainPattern);
if (match && match[1] !== 'api') {
const mainDomain = match[2];
const newUrl = `https://api.${mainDomain}${req.originalUrl}`;
// 308: permanent move, preserve method
return res.redirect(308, newUrl);
}
next();
});
// HTTP to HTTPS enforcement (permanent)
app.enable('trust proxy'); // Trust X-Forwarded-Proto from load balancer
app.use((req, res, next) => {
if (req.protocol !== 'https' && process.env.NODE_ENV === 'production') {
// Permanent redirect to HTTPS
const httpsUrl = `https://${req.headers.host}${req.originalUrl}`;
// Set HSTS header before redirecting
res.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
return res.redirect(308, httpsUrl);
}
next();
});
// Service consolidation
const SERVICE_MIGRATIONS = {
'payments-api.example.com': 'api.example.com/payments',
'users-api.example.com': 'api.example.com/users',
'inventory-api.example.com': 'api.example.com/inventory'
};
app.use((req, res, next) => {
const host = req.headers.host;
const newBasePath = SERVICE_MIGRATIONS[host];
if (newBasePath) {
const newUrl = `https://${newBasePath}${req.originalUrl}`;
// Permanent service consolidation
return res.redirect(308, newUrl);
}
next();
});
// Path restructuring
const PATH_MIGRATIONS = {
'/old-api/resource': '/api/v2/resources',
'/legacy/endpoint': '/api/v2/endpoint',
'/deprecated/action': '/api/v2/actions'
};
app.use((req, res, next) => {
const newPath = PATH_MIGRATIONS[req.path];
if (newPath) {
return res.redirect(308, newPath);
}
next();
});

Deep Dive

The 308 Permanent Redirect status code, specified in RFC 9110 Section 15.4.9, indicates that the target resource has been assigned a new permanent URI and any future references to this resource ought to use one of the enclosed URIs. The server SHOULD generate a Location header containing a preferred URI reference. Per RFC 9110, the user agent MUST NOT automatically change the request method when performing the redirect - this strict method preservation distinguishes 308 from 301. The 308 status code was introduced via RFC 7538 (April 2015) to provide a method-preserving counterpart to 301, paralleling how 307 provides method preservation for the temporary redirect case that 302 handles ambiguously.

Technical Details

RFC evolution: RFC 7238 (June 2014) initially defined 308 as Experimental status, addressing the long-standing issue that 301 was specified to preserve methods but browsers historically changed POST→GET. RFC 7538 (April 2015) obsoleted RFC 7238, moving 308 to Standards Track after implementation experience showed it was safe and useful. RFC 9110 (June 2022) incorporated 308 into the unified HTTP Semantics specification, cementing it as the official permanent method-preserving redirect. The RFC history reflects HTTP Working Group’s careful approach: Experimental first (verify no unexpected interactions), then Standards Track (proven interoperable), finally core specification (fundamental to modern HTTP).\n\nMethod preservation semantics are identical to 307: the redirected request MUST use the same HTTP method and include the same request body. For POST, PUT, PATCH, DELETE, the entire request body is resent to the redirect target. Request headers generally carry over, with hop-by-hop headers (Connection, Transfer-Encoding) stripped and Host updated to match redirect target. Authorization headers may be dropped when redirecting cross-origin (RFC 9110 doesn’t mandate this, but many clients implement it for security). The key semantic distinction from 307: 308 signals permanence - clients should update stored URLs, bookmarks, and cached endpoint references. Some HTTP client libraries automatically update base URLs when receiving 308, while 307 leaves original URLs unchanged for future requests.\n\nCaching implications per RFC 9110: 308 responses are cacheable by default unless explicitly forbidden by Cache-Control or method definition. Caching a 308 means storing the redirect mapping permanently (or until cache expiry). Example: Cache stores “GET /api/v1/users → 308 → /api/v2/users” with max-age=86400. For 24 hours, requests to /api/v1/users immediately redirect to /api/v2/users without contacting origin. This differs from 307 caching: 308’s permanence justifies longer cache TTLs (days/weeks vs minutes for 307). CDNs can aggressively cache 308 mappings, reducing origin load. Best practice: include Cache-Control: public, max-age=31536000, immutable for truly permanent 308s (one year, immutable means never revalidate). For migrations still in progress, use shorter TTLs: Cache-Control: public, max-age=86400.\n\nSEO and search engine behavior: Google’s John Mueller confirmed (2019) that Googlebot treats 308 identically to 301 - full PageRank transfer, permanent URL replacement in search index, canonical URL updated to redirect target. Bing and other search engines follow similar policies. The permanence signal (308/301 vs 307/302) determines whether link equity transfers and URLs are replaced in SERPs. Method preservation (308/307 vs 301/302) affects how search engines handle form submissions and POST-based search forms (rare in modern web but relevant for some applications). For SEO migrations, 308 and 301 are functionally equivalent; choose 308 for A…

Code Example

// Enterprise-grade 308 Permanent Redirect implementation\nconst express = require('express');\nconst app = express();\n\n// Comprehensive redirect configuration management\nclass PermanentRedirectManager {\n constructor() {\n this.redirects = new Map();\n this.metrics = {\n totalRedirects: 0,\n redirectsByPath: new Map()\n };\n }\n \n // Register permanent redirect\n addRedirect(fromPath, toPath, metadata = {}) {\n this.redirects.set(fromPath, {\n target: toPath,\n createdAt: new Date(),\n reason: metadata.reason || 'migration',\n expiresAt: metadata.expiresAt || null // When redirect can be removed\n });\n }\n \n // Get redirect target\n getRedirect(path) {\n return this.redirects.get(path);\n }\n \n // Record redirect usage for analytics\n recordRedirect(fromPath, clientInfo) {\n this.metrics.totalRedirects++;\n \n const pathMetrics = this.metrics.redirectsByPath.get(fromPath) || {\n count: 0,\n clients: new Set()\n };\n \n pathMetrics.count++;\n pathMetrics.clients.add(clientInfo.userAgent);\n \n this.metrics.redirectsByPath.set(fromPath, pathMetrics);\n \n // In production: send to monitoring service\n console.log(`308 Redirect: ${fromPath} -> ${this.redirects.get(fromPath).target}`);\n }\n \n // Generate migration status report\n getMetricsReport() {\n const report = {\n totalRedirects: this.metrics.totalRedirects,\n activeRedirects: this.redirects.size,\n byPath: []\n };\n \n for (const [path, data] of this.redirects.entries()) {\n const metrics = this.metrics.redirectsByPath.get(path);\n \n report.byPath.push({\n from: path,\n to: data.target,\n reason: data.reason,\n createdAt: data.createdAt,\n usageCount: metrics?.count || 0,\n uniqueClients: metrics?.clients.size || 0\n });\n }\n \n return report;\n }\n}\n\nconst redirectManager = new PermanentRedirectManager();\n\n// Register API v1 → v2 migrations\nredirectManager.addRedirect('/api/v1/users', '/api/v2/users', {\n reason: 'API v2 migration',\n expiresAt: new Date('2026-06-01') // Can remove redirect after this date\n});\n\nredirectManager.addRedirect('/api/v1/orders', '/api/v2/orders', {\n reason: 'API v2 migration',\n expiresAt: new Date('2026-06-01')\n});\n\nredirectManager.addRedirect('/api/v1/products', '/api/v2/products', {\n reason: 'API v2 migration',\n expiresAt: new Date('2026-06-01')\n});\n\n// Middleware to handle permanent redirects\napp.use((req, res, next) => {\n const redirect = redirectManager.getRedirect(req.path);\n \n if (redirect) {\n // Record metrics\n redirectManager.recordRedirect(req.path, {\n userAgent: req.headers['user-agent'],\n ip: req.ip,\n method: req.method\n });\n \n // Set aggressive caching for permanent redirects\n res.set('Cache-Control', 'public, max-age=31536000, immutable');\n \n // Add dep...

Frequently Asked Questions

What's the difference between 308 Permanent Redirect and 301 Moved Permanently?

308 guarantees the HTTP method and request body are preserved during the redirect - POST stays POST with the same body. 301 historically allowed (and caused) method changes - browsers converted POST to GET. For APIs and forms requiring method preservation, use 308. For traditional web pages (mostly GET requests), 301 is fine. Both transfer PageRank and SEO value equally.

Should I use 307 or 308 for API endpoint migration?

Use 308 for permanent migrations where the old endpoint is deprecated forever. Use 307 for temporary migrations during testing or gradual rollout. Example: Permanently retiring /api/v1 in favor of /api/v2 - use 308. Testing /api/v2 with 10% of traffic before full migration - use 307. The key: 308 signals permanence, clients should update URLs; 307 is temporary, keep using original URL.

Does 308 Permanent Redirect affect SEO?

No negative impact - Google treats 308 identically to 301 for SEO purposes. Both transfer full PageRank and link equity, both cause permanent URL replacement in search index. Use 308 for APIs or forms requiring method preservation, 301 for traditional page redirects. Search engines care about the permanence signal (308/301 vs 307/302), not the method preservation aspect.

Are 308 redirects cacheable?

Yes, 308 responses are cacheable by default per RFC 9110. Because 308 signals permanence, aggressive caching is appropriate - use Cache-Control: public, max-age=31536000, immutable for truly permanent redirects (one year, never revalidate). Clients can store the redirect mapping permanently and update their base URLs to skip future redirects entirely.

What happens if legacy clients don't support 308?

Modern browsers (Chrome 48+, Firefox 43+, Safari 10.1+) and HTTP libraries all support 308. Very old clients (IE 10, Java 7, pre-2015 libraries) might not recognize it and could fail. For public APIs, audit client analytics before deploying 308. For controlled environments (internal APIs, modern mobile apps), 308 is safe. Worst case: unsupported clients treat 308 as error, don't follow redirect - they won't silently break by changing methods like with 301.

Common Causes

  • Permanent API endpoint migration from v1 to v2
  • Domain consolidation moving microservices to unified API domain
  • HTTP to HTTPS permanent upgrade enforcing secure connections
  • Service consolidation combining separate APIs into single endpoint
  • URL structure reorganization with permanent path changes
  • Permanent subdomain migration (api-v1.example.com to api.example.com)
  • Legacy endpoint retirement redirecting to modern replacement
  • Brand or company rename requiring permanent API domain change

Implementation Guidance

  • Use 308 for permanent API migrations where method preservation is critical
  • For web pages (GET requests), 301 is fine; use 308 for clarity or when POST is possible
  • Set Cache-Control: public, max-age=31536000, immutable for truly permanent 308s
  • Add Deprecation header and Link rel=alternate to inform clients of migration
  • Monitor redirect usage metrics to identify when clients have migrated and 308s can be removed
  • Update client code to use new URLs directly when receiving 308 - eliminate future redirects
  • For HTTPS upgrades, combine 308 with HSTS header to prevent downgrade attacks
  • Validate redirect targets to prevent open redirect vulnerabilities
  • After sufficient transition period, replace 308 with 410 Gone to force client upgrades
  • Document migration timeline and new endpoints for API consumers

Comments