Skip to content

HTTP status code · Redirection (3xx)

308 Permanent Redirect

308 Permanent Redirect means the resource has moved for good to the URL in Location, and the client must repeat the request there with the same method and body. It is the permanent counterpart of 307 and the safe choice for moving API endpoints or any URL that receives POST.

Facts about this status code
Class3xx, Redirection
Defined inRFC 9110 §15.4.9
Cacheable by defaultYes, heuristically cacheable
Safe to retryRepeat the same request at Location and use that URL from now on
Relevant headers
  • Location: the new permanent URL; the method and body go there unchanged
  • Cache-Control: like 301, cached by browsers unless told otherwise

What 308 means

308 was introduced by RFC 7238 in 2014, promoted to Proposed Standard by RFC 7538 in 2015, and now lives in RFC 9110, section 15.4.9. It means the same as 301, a new permanent URI, but rules out the POST-to-GET switch that browsers apply to 301. It is heuristically cacheable, so browsers remember it just as they remember a 301.

Google handles 308 exactly like 301, as a strong signal that the target should become canonical. Next.js sends 308 by default for next.config redirects with permanent: true, for permanentRedirect() and for its trailingSlash normalization, so many sites emit it without ever choosing it.

Every current browser and mainstream HTTP library follows it. The one old gap was Internet Explorer 11 on Windows 7 and 8.1, which did not understand 308; if you still serve such clients, 301 is the conservative choice for pages fetched with GET.

When to use it

  • Moving an API version or endpoint permanently, such as /api/v1/upload to /api/v2/upload, while clients keep sending POST.
  • Canonical host or HTTPS redirects on sites that also receive form posts or webhooks.
  • Anywhere you would reach for 301 but need the method and body preserved.

Common causes

If you are visiting the site

  • ERR_TOO_MANY_REDIRECTS from a 308 slash or host rule fighting another rule; because the browser caches the 308, the loop can persist after the server is fixed.

If you run the server

  • Next.js trailingSlash (or a permanent: true redirect) disagreeing with a CDN or proxy rule that adds or removes the slash the other way.
  • Webhook senders and API clients still calling an old endpoint: the 308 keeps well-behaved clients working, but some HTTP clients do not follow redirects on POST and log the 308 as a failure.
  • Streamed request bodies: fetch() cannot replay a body sent as a ReadableStream, so an upload that hits a 308 (or a 307) fails instead of being redirected.

How to fix it

If you are visiting the site

  • Clear the cached files for the site to drop a stale 308, or check the page in a private window.

If you run the server

  • Map the chain with curl -sIL and make the slash and host rules agree, enforced in a single layer.
  • Ask webhook senders to update their URL and keep the 308 until traffic to the old path stops.
  • For streamed uploads, call the final URL directly instead of relying on the redirect.

How to send 308

Express (Node.js)
// v1 is retired; clients keep POSTing, so the method must survive
app.use('/api/v1', (req, res) => {
  // req.url is the path after the mount point, query string included
  res.redirect(308, '/api/v2' + req.url);
});
Next.js App Router route handler
// next.config.js: permanent: true sends 308
module.exports = {
  async redirects() {
    return [
      { source: '/api/v1/:path*', destination: '/api/v2/:path*', permanent: true }
    ];
  }
};

// In a route handler: Response.redirect(new URL('/api/v2/upload', request.url), 308)
// In pages and Server Components: permanentRedirect('/new') from next/navigation
Go net/http
mux.HandleFunc("/api/v1/", func(w http.ResponseWriter, r *http.Request) {
	target := "/api/v2/" + strings.TrimPrefix(r.URL.Path, "/api/v1/")
	http.Redirect(w, r, target, http.StatusPermanentRedirect) // 308
})
Python FastAPI
from fastapi import FastAPI
from fastapi.responses import RedirectResponse

app = FastAPI()

@app.api_route("/api/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
def v1_moved(path: str):
    return RedirectResponse(f"/api/v2/{path}", status_code=308)
Nginx
# rewrite ... permanent would send 301; return gives a 308
location ~ ^/api/v1/(.*)$ {
  return 308 /api/v2/$1$is_args$args;
}

Commonly confused with

308 vs 301
Same permanence and the same SEO treatment; 301 lets clients re-send POST as GET, 308 does not.
308 vs 307
307 keeps the method too but is temporary: not cached by default and not a canonical signal for search engines.

Frequently asked questions

What is the difference between 301 and 308?
Both say the resource moved permanently and both are cached by browsers. A 301 lets the client change POST to GET on the next request, and browsers do; a 308 requires the same method and body.
Is a 308 redirect good for SEO?
Yes. Google's documentation lists 308 as equivalent to 301: a strong signal that the redirect target should be the canonical URL.
Why does Next.js return 308 instead of 301?
Next.js uses 308 for permanent: true redirects, permanentRedirect() and trailingSlash handling so that non-GET requests keep their method. If you need a literal 301, set statusCode: 301 instead of permanent in the next.config redirect.
Do all browsers support the 308 status code?
All current browsers do. Internet Explorer 11 only followed 308 on Windows 10; on Windows 7 and 8.1 it showed an error page instead.

Last reviewed by Arielton Oberek.