Five codes, two questions
301 and 302 were specified to keep the method, and every browser did the opposite, so 303, 307 and 308 exist to say what you actually mean. If you are redirecting anything other than a plain page navigation, use one of those three.
| Code | Permanent? | Keeps the method? | For |
|---|---|---|---|
| 301 Moved Permanently | yes — cached hard | no: POST becomes GET | a URL that has moved for good |
| 302 Found | no | no: POST becomes GET | a temporary detour (the vague one) |
| 303 See Other | no | never: always GET | after a form post — see below |
| 307 Temporary Redirect | no | yes, with the body | a temporary detour for an API |
| 308 Permanent Redirect | yes | yes, with the body | a moved API endpoint |
POST-redirect-GET, and the refresh that charges twice
Answer a successful form submission with a redirect to the result rather than with the page itself. The address bar then holds a URL that can be refreshed, bookmarked and shared — and the double-submit problem disappears without any JavaScript.
POST /orders → 201, and the browser shows the result
# the user presses refresh → the browser re-sends the POST → a second order
POST /orders → 303 See Other, Location: /orders/482
GET /orders/482 → 200
# refresh now re-sends the GET, which is harmless
301 is close to permanent
Search engines treat 301 as "move the ranking", which is why it is the right answer for a genuine move — and why it is worth getting right first time.
Ship a 301 to the wrong place and visitors keep going there after you fix it — their browser never asks again. Use 302 or 307 while you are unsure, and add Cache-Control: max-age=3600 to a 301 so there is a way back.
Chains, loops and the cost
Collapse chains into one hop wherever you can: redirect straight to the final URL. Browsers stop after about twenty hops and show an error; a loop between two rules is the classic cause, usually one in the CDN and one in the application, each unaware of the other.
http://doors.test → 301 → https://doors.test
https://doors.test → 301 → https://www.doors.test
https://www.doors.test → 302 → https://www.doors.test/en
# three round trips before a single byte of HTML
A Strict-Transport-Security header tells the browser to go straight to https next time, without asking http first. That is one round trip saved on every first visit, and one fewer moment where a plain-text request exists at all.
In code
// Express
res.redirect(308, "/api/v2/orders");
// Fetch follows redirects by default and hides them
const res = await fetch("/old");
res.redirected // true
res.url // the final URL
// or handle them yourself
await fetch("/old", { redirect: "manual" }); // opaque response, status 0
await fetch("/old", { redirect: "error" }); // reject instead