DEVESSENTIALS

HTTP 308 Permanent Redirect

308 is to 301 what 307 is to 302: the same meaning (moved permanently), but the client must not turn a POST into a GET when following it. It was introduced in 2015 by RFC 7538 and is now part of the core HTTP spec, RFC 9110.

3xx · RedirectionRFC 9110 §15.4.9

308 Permanent Redirect

Permanently moved: repeat this and future requests at the Location URL without changing the method.

Who fixes it: Server config. Clients follow it. The site owner controls where it points and has to avoid redirect loops.

What 308 means

It's cacheable by default, so browsers remember it like a 301.

Modern platforms send a lot of 308s: Next.js permanent: true redirects, permanentRedirect() and trailing-slash normalisation all use it.

Source: RFC 9110 §15.4.9.

Where 308s come from

  • Next.js trailing slash handling: /about/ redirects to /about (or the reverse with trailingSlash: true) with a 308.
  • permanent: true redirects in next.config, or permanentRedirect().
  • HTTP to HTTPS redirects on hosting platforms and CDNs that prefer method-preserving redirects.
  • API versioning: POST /v1/orders permanently moved to /v2/orders without breaking clients that POST.
  • Redirect loops (ERR_TOO_MANY_REDIRECTS) when a proxy terminates TLS and the app, which sees plain HTTP, keeps redirecting to HTTPS. Fix it by trusting X-Forwarded-Proto.

How to set up a 308

nginx

rewrite ... permanent sends 301, so use return 308 when you need the method preserved.

server {
    listen 80;
    server_name example.com;
    return 308 https://example.com$request_uri;
}

# Keep POST/PUT bodies intact for API clients
location ~ ^/api/v1/(.*)$ {
    return 308 /api/v2/$1$is_args$args;
}

Express, behind a load balancer

Without trust proxy, req.secure is false for every request that arrived over HTTPS at the load balancer, and this middleware loops forever.

app.set('trust proxy', 1); // read X-Forwarded-Proto from the load balancer

app.use((req, res, next) => {
  if (req.secure) return next();
  res.redirect(308, `https://${req.headers.host}${req.originalUrl}`);
});

Next.js

permanent: true sends 308.

// next.config.ts
const nextConfig = {
  async redirects() {
    return [{ source: '/docs/:path*', destination: '/guides/:path*', permanent: true }];
  },
};
export default nextConfig;

Check it

Look for a single hop that ends in 200.

curl -sIL http://example.com/docs/setup | grep -iE '^(HTTP|location)'

Headers that matter with 308

Location
The new permanent URL.
Cache-Control
Like 301, a 308 may be cached indefinitely without it. Use a short max-age while testing.

308 vs similar status codes

SEO impact of 308

Google treats 308 the same as 301: a strong signal that the target is the canonical URL, with ranking signals consolidated there. The usual redirect rules apply: one hop, the closest equivalent page, keep it for at least a year, and update internal links and sitemaps.

Related status codes

Looking for another code? Search the full HTTP status codes list, or read which code your API should return in the REST API status codes guide.

Learn more

Browse all developer guides →

Frequently Asked Questions

Is 308 better than 301?▾

For APIs, yes, because it guarantees POST, PUT and DELETE requests are re-sent unchanged. For ordinary pages they're equivalent: browsers follow both and Google treats both as permanent. Internet Explorer 11 on Windows 7 and 8.1 doesn't understand 308, which is the only reason some sites still prefer 301.

Why does my Next.js site return 308 for trailing slashes?▾

Next.js normalises URLs. With the default trailingSlash: false, /about/ permanently redirects to /about with a 308; with trailingSlash: true it's the other way round. It's intentional, so each page has a single canonical URL.

How do I fix ERR_TOO_MANY_REDIRECTS with a 308 loop?▾

Usually TLS ends at a proxy or CDN, the app sees plain HTTP and redirects to HTTPS forever. Make the app trust the proxy's X-Forwarded-Proto header (app.set('trust proxy') in Express), or do the HTTPS redirect only at the edge. With Cloudflare, use the Full (strict) SSL mode instead of Flexible.