# 308 Permanent Redirect

> Permanent redirect that preserves the HTTP method. Learn when to use 308 instead of 301 for method-sensitive permanent redirects.

Source: https://howhttpworks.com/status-codes/308
Last reviewed: 2026-10-04

> **TL;DR:** 308 Permanent Redirect is 301 with a guarantee: the client must repeat the same method and body at the new `Location`, and the move is permanent and cacheable. Use it when an API endpoint or form target moves and POST must stay POST.

## What it means

```http
PUT /v1/users/42 HTTP/1.1
Host: api.example.com

HTTP/1.1 308 Permanent Redirect
Location: https://api.example.com/v2/users/42
```

The client retries `PUT /v2/users/42` with the same body (RFC 9110 section 15.4.9, originally RFC 7538). Like 301, it is cacheable by default, so clients may skip the old URL afterwards.

## 301 or 308?

- **Moving GET pages for SEO:** either works. Google treats 301 and 308 as the same permanent signal. 301 is the more common choice and understood by every tool.
- **Anything with a request body** (POST, PUT, PATCH, DELETE): use 308. A 301 lets clients turn POST into GET, silently dropping the body and often producing a confusing 404 or 405 at the new URL.
- **Next.js `redirects()` with `permanent: true`** emits 308, not 301.
- A few very old clients and embedded SDKs do not understand 308 and do not follow it. Rare today, but check legacy integrations.

## Gotchas

- **Caching makes mistakes sticky.** Browsers cache 308 like 301. Test with [307](https://howhttpworks.com/status-codes/307) first and switch once certain, or bound it with `Cache-Control: max-age=...`.
- **Request bodies must be replayable**; streaming uploads often cannot follow a 308 automatically.
- **Cross-origin redirects drop `Authorization`** in browsers and most clients, so authenticated API calls may reach the new host as 401.
- **Preflight requests cannot be redirected.** A CORS preflight answered with 308 fails with "Redirect is not allowed for a preflight request".

## Implementation

### nginx

```nginx
location /v1/ {
    return 308 https://api.example.com/v2$request_uri;
}
```

### Express

```javascript
// Express 5 syntax; in Express 4 use '/v1/*'
app.all('/v1/*splat', (req, res) => {
  res.redirect(308, `/v2${req.originalUrl.slice(3)}`)
})
```

### Apache

```apache
RedirectMatch 308 ^/v1/(.*)$ /v2/$1
```

## Reproduce with curl

```bash
curl -sI https://api.example.com/v1/users/42
# HTTP/2 308
# location: https://api.example.com/v2/users/42

# Confirm the method survives the hop
curl -sv -L -X POST -d '{"a":1}' -H 'Content-Type: application/json' \
  https://api.example.com/v1/users 2>&1 | grep -E '^> (POST|GET)'
```

Both request lines should be `POST`. If the second is `GET`, the redirect was a 301/302 or the client rewrote it.

## Migration plan

1. Ship the new endpoint and keep the old one answering 308 to it.
2. Log traffic still hitting the old path and announce a deprecation date.
3. Update SDKs and docs to the new URL.
4. Only after traffic drops, consider returning [410 Gone](https://howhttpworks.com/status-codes/410).

## Related

- [301 Moved Permanently](https://howhttpworks.com/status-codes/301)
- [307 Temporary Redirect](https://howhttpworks.com/status-codes/307)
- [Location header](https://howhttpworks.com/headers/location)
- [301 vs 302](https://howhttpworks.com/compare/301-vs-302)
