HTTP 301 is Moved Permanently: the resource has a new URL for good, given in the Location header. RFC 9110 section 15.4.2 asks clients that can to update their stored links. For historical reasons it also lets a user agent turn a POST into a GET when it follows the redirect; when the method must survive, use 308.
The exchange
GET /blog/old-post HTTP/1.1
Host: example.com
HTTP/1.1 301 Moved Permanently
Location: https://example.com/articles/new-post
Browsers follow the Fetch standard here: after a 301 they turn a POST into a GET without its body, and keep every other method. curl -L does the same unless you pass --post301.
Caching: it sticks
301 is heuristically cacheable (RFC 9110 section 15.1), and browsers keep a 301 that has no cache headers, often for a long time. You can’t recall it from a visitor’s cache, so a wrong 301 keeps sending people to the wrong place after you’ve fixed the server. While testing, send Cache-Control: max-age=3600 with it, or test with 302 and switch once it’s right.
Search engines
Google treats 301 and 308 the same: a strong signal that the target should be the canonical URL. 302 and 307 are only a weak signal. Google’s site-move guide says to keep the redirects for as long as possible, generally at least a year. Point each old URL straight at its final address: Googlebot follows at most 10 redirect hops, and every hop costs visitors a round trip.
Who sends it
- nginx and Go’s
ServeMux, when a directory is requested without its trailing slash. - CloudFront, under the “Redirect HTTP to HTTPS” viewer protocol policy.
- AWS ALB redirect actions, which offer
HTTP_301andHTTP_302only.
301 or another redirect
Use 308 for an endpoint that takes POST, PUT or DELETE, such as an API route or a form’s action. Use 302, 303 or 307 when the move may be undone: a sale page, a login bounce, an A/B test.
In your stack
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
Redirect permanent /blog/old-post /articles/new-post
res.redirect(301, '/articles/new-post'); // Express; the default is 302
return redirect("/articles/new-post", code=301) # Flask
return redirect("/articles/new-post", permanent=True) # Django
http.Redirect(w, r, "/articles/new-post", http.StatusMovedPermanently)