# 301 vs 308: Permanent Redirects and POST Requests

> 301 vs 308: both signal a permanent move, but only 308 preserves POST. Compare browser handling, Google indexing, cache controls, and curl traces.

Source: https://howhttpworks.com/compare/301-vs-308
Last reviewed: 2026-10-05

> **TL;DR:** For a permanent move of a GET page, either 301 or 308 works. Use 308 when a redirected POST must arrive as POST with its body intact. Google treats both as permanent redirects; method preservation is the deciding difference.

## Side By Side

| Behavior | 301 Moved Permanently | 308 Permanent Redirect |
|---|---|---|
| Meaning | Resource has a new permanent URL | Resource has a new permanent URL |
| Redirected GET | Remains GET | Remains GET |
| Redirected POST in browser Fetch | Becomes GET; body discarded | Method and replayable body preserved |
| HTTP method rule | POST-to-GET conversion is permitted | Method must be preserved when following automatically |
| Cache eligibility | Heuristically cacheable, subject to method and cache controls | Same |
| Google Search | Permanent redirect; destination is a canonicalization signal | Same documented treatment |

The status semantics come from [RFC 9110 §15.4.2](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.2) and [§15.4.9](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.9). Browser method handling follows the [Fetch redirect algorithm](https://fetch.spec.whatwg.org/#http-redirect-fetch). The table assumes the client follows the redirect and can replay the body: Fetch can fail a redirect when the request body has no replayable source.

## When To Use Each

**301: moving a document URL.** A request for `/docs/old-install` should lead to `/docs/install`. With GET, there is no POST body to lose. This is the kind of permanent URL move covered by [MDN's 301 reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/301).

Illustrative exchange:

```http
GET /docs/old-install HTTP/1.1
Host: example.com

HTTP/1.1 301 Moved Permanently
Location: /docs/install
Cache-Control: max-age=300
Content-Length: 0
```

**308: moving a write endpoint.** Suppose `/api/v1/events` is retired in favor of `/api/events`, which accepts the same payload. Redirect before processing the event at the old endpoint. [308 preserves the method and body](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/308); the destination receives the write.

Illustrative response to a POST at the old endpoint:

```http
HTTP/1.1 308 Permanent Redirect
Location: /api/events
Cache-Control: no-store
Content-Length: 0
```

Keep the two jobs distinct in your routing code: either process the write or redirect it. A 308 after processing the event asks the client to submit the same event again at the destination.

## Caching And Google Search

Both statuses permit heuristic caching. That does **not** specify a universal browser TTL, guarantee storage of a POST response, or mean “forever.” [RFC 9111 §4.2.2](https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.2) leaves the heuristic algorithm unspecified. For a GET migration, the example's `max-age=300` chooses five minutes; it is a rollout choice, not a default. `no-store` instructs caches not to store the response ([§5.2.2.5](https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.5)).

[Google's redirect documentation](https://developers.google.com/search/docs/crawling-indexing/301-redirects) places **301 and 308 in the permanent category**: Googlebot follows them, and the indexing pipeline uses the destination as a signal for canonicalization. This is a signal, not a promise that the old URL disappears immediately. Google also documents that an old URL can remain an alternate name and occasionally appear in results.

## The Common Mistake

Testing only a GET, then shipping a 301 rule across an API. Both redirects look fine when you click the URL; a POST can lose its body under 301. Test the methods that actually reach that route.

The opposite mistake is using 308 after a form submission when you want the next request to fetch a receipt page. That is a [303 See Other](https://howhttpworks.com/status-codes/303) flow: the redirect should lead to a retrieval, as defined in [RFC 9110 §15.4.4](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.4).

## Check On The Wire

Inspect the initial status, destination, and cache policy without following:

```bash
curl -sS -D - -o /dev/null https://example.com/docs/old-install
```

On your staging endpoint, trace a POST through the redirect:

```bash
curl -sS -v -L --data 'event=probe' \
  -o /dev/null https://example.com/api/v1/events 2>&1 |
  grep -E '^([<>] (POST|GET|HTTP/)|< [Ll]ocation:|< [Cc]ache-[Cc]ontrol:)'
```

Look for `> POST` at the first URL and the method at the second URL. Expect GET after 301 and POST after 308 with this command. Avoid `-X POST` and `--post301`: they override the method behavior being measured. These options and redirect rules are documented in the [curl manual](https://curl.se/docs/manpage.html#-L).

In Chrome DevTools, enable **Network → Preserve log** before submitting, then inspect each request's method, payload, and `Location`. If cached routing obscures your change, repeat with **Disable cache** enabled while DevTools is open. See the [Network reference](https://developer.chrome.com/docs/devtools/network/reference).

## Related

- [301 Moved Permanently](https://howhttpworks.com/status-codes/301) and [308 Permanent Redirect](https://howhttpworks.com/status-codes/308)
- [301 vs 302](https://howhttpworks.com/compare/301-vs-302) for permanent versus temporary moves
- [Cache-Control](https://howhttpworks.com/headers/cache-control) for explicit redirect cache policy
