Short answer: 301 Moved Permanently is the server telling the client that the URL has a new, permanent address, which it names in the Location header. It is not an error. Browsers follow it on their own, search engines move the old URL’s place in their index to the new one, and browsers may cache it. If you see a page that just says “301 Moved Permanently”, the client did not follow the redirect: curl without -L, a script, or a response without a usable Location.
HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Wed, 21 Oct 2026 12:21:10 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
Location: https://kernel.org/That is a typical one: a plain HTTP request answered with a 301 to the HTTPS address. The 162 bytes of body are nginx’s default page, the one people see when nothing follows the redirect:
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>301 vs 302 vs 307 vs 308: which redirect to use
HTTP has four redirects that point to a new address, plus 303 for a special case. They differ in two ways: whether the move is permanent, and whether the client may turn a POST into a GET when it follows.
| Code | Permanent | Method when followed | Cached without headers | Use for |
|---|---|---|---|---|
| 301 Moved Permanently | Yes | POST may become GET | Yes | Moved pages, HTTP to HTTPS, www to bare domain |
| 302 Found | No | POST may become GET | No | Short-term moves, login and language redirects |
| 303 See Other | No | Always GET | No | After a form POST: show the result page |
| 307 Temporary Redirect | No | Kept, body included | No | Temporary move of a form or API endpoint |
| 308 Permanent Redirect | Yes | Kept, body included | Yes | Permanent move of a form or API endpoint |
“Cached without headers” is what RFC 9110 calls heuristically cacheable: a client may keep a 301 or 308 even when the response says nothing about caching. The method column is where 301 surprises people. RFC 9110 explains that early browsers turned a redirected POST into a GET, practice settled on that, and the spec now allows it “for historical reasons”. 307 and 308 were added later to keep the method. Here is what curl 7.88.1 on Debian 12 did when it followed each code with -L, posting a form to a small local test server that printed what arrived:
301 -> GET /new body=""
302 -> GET /new body=""
303 -> GET /new body=""
307 -> POST /new body="name=test"
308 -> POST /new body="name=test"So a form or API call sent to an address that answers 301 arrives at the new address as an empty GET, and the endpoint answers with an error or a page instead of doing the work. curl has --post301 to keep the POST, as its man page explains, but browsers have no such switch. For anything that receives POSTs, redirect with 308, or better, send the client to the final URL in the first place.
Why a 301 sticks in the browser
Because a 301 may be cached without any headers, browsers keep them, often for a long time, and reuse them without asking the server again. A wrong 301 therefore keeps sending people to the wrong place after you fix the rule on the server. Two habits prevent that:
- Test with 302, switch to 301 when the rule is right. A 302 is not cached unless you say so, so every reload asks the server again.
- Give the 301 an expiry. nginx’s
add_headerapplies to 301 responses without thealwaysflag, soadd_header Cache-Control "max-age=3600";in the redirecting block limits the browser to an hour.
To get rid of a cached 301 in your own browser, open the address in a private window, or tick Disable cache in the DevTools Network tab and reload, or clear cached images and files for that site. A 307 Internal Redirect in Chrome DevTools is not from your server at all: it is the browser applying HSTS, as the redirect loop guide explains.
What does a 301 do for SEO?
Google’s documentation on HTTP status codes describes 301 and 308 as a strong signal that the redirect target should be processed, and 302, 303 and 307 as a weak one. In practice a 301 means Google indexes and shows the new URL in place of the old one, while after a 302 the old URL may stay in results. Google’s crawlers follow up to 10 redirect hops, and the content of the redirecting URL itself is ignored. What follows from that:
- Use 301 or 308 for every permanent move: HTTP to HTTPS, one hostname to another, a changed URL structure, a merged page.
- Redirect each old URL to its exact new equivalent. Sending everything to the home page is the classic mistake; search engines may treat it as a soft 404. For content that is gone without a replacement, a 404 or 410 is the honest answer, as covered in 404 Not Found.
- Avoid chains.
http://www.example.comtohttps://www.example.comtohttps://example.comis two hops where one will do. Point every old address straight at the final one. - Keep the redirects. Old links, bookmarks and other sites keep pointing at the old address for years. Update your own internal links and sitemap to the new URLs as well, so your site does not depend on its own redirects.
How to set up a 301 in nginx
return 301 is the simplest and fastest form: nginx answers at once without looking at the file system.
# one page
location = /old-page {
return 301 /new-page;
}
# a whole section, keeping the rest of the path
location /blog/ {
rewrite ^/blog/(.*)$ /articles/$1 permanent;
}rewrite ... permanent sends a 301; the redirect flag sends a 302. A whole domain moves with a server block that does nothing else:
server {
listen 80;
listen 443 ssl;
server_name old-domain.com www.old-domain.com;
ssl_certificate /etc/letsencrypt/live/old-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/old-domain.com/privkey.pem;
return 301 https://new-domain.com$request_uri;
}$request_uri carries the path and query string across, so every old page lands on its counterpart. The old domain still needs a valid certificate, or HTTPS visitors get a certificate warning before they ever see the redirect. For the usual HTTP to HTTPS and www to bare domain redirects of one site, use separate blocks that redirect and never serve content, as laid out in the redirect loop guide; the HTTPS side with certificates is in the nginx reverse proxy guide. Then test and reload:
sudo nginx -t && sudo systemctl reload nginx301s that nginx sends on its own
Two redirects appear without any return in your config. A request for a directory without its trailing slash, /docs for /docs/, gets a 301 to the slash version. And per nginx’s documentation, a location whose prefix ends in a slash and that passes requests on with proxy_pass (or fastcgi_pass and the like) answers a request for the same path without the slash with a 301. A POST to /api then turns into a GET to /api/, the effect shown in the curl test above. Call the exact path, or add an exact-match location = /api for it.
When nginx builds such a redirect from a local path, it makes it absolute from the request’s scheme, the host and, by default, the port it listens on. Behind another proxy or a TLS terminator that can produce http:// or :8080 in the Location. absolute_redirect off; (nginx 1.11.8 and later) sends a relative Location instead, which the browser resolves against the address it actually used.
How to set up a 301 in Apache and .htaccess
Apache has two tools. mod_alias handles simple moves:
Redirect 301 /old-page https://example.com/new-page
RedirectMatch 301 ^/blog/(.*)$ https://example.com/articles/$1Redirect matches a path prefix and appends the rest, so Redirect 301 /shop https://example.com/store also sends /shop/item?id=7 to /store/item?id=7. Leave out the code and Apache sends a 302, per its documentation; permanent is a synonym for 301.
mod_rewrite handles conditions. This is the common pair, HTTP to HTTPS and www to the bare domain, written so every variant gets there in one hop:
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [L,R=301]Write R=301 explicitly: a bare [R] sends a 302. The query string is passed along on its own. On WordPress, put the rules above the # BEGIN WordPress block, which WordPress rewrites itself, and do not mix Redirect and RewriteRule for the same URLs; once a site uses mod_rewrite, keep its redirects there too. Changes in .htaccess apply to the next request. In a VirtualHost, run sudo apachectl configtest && sudo systemctl reload apache2.
From PHP
header('Location: https://example.com/new-page', true, 301);
exit;The third argument matters: per the PHP manual, a Location header on its own is sent with a 302. The exit stops the script from running on after the redirect.
How do I check a 301?
curl -sI http://example.com/old-page # one response, headers only
curl -sSIL http://example.com/ | grep -iE '^(HTTP|location)' # follow the whole chain
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://example.com/HTTP/1.1 301 Moved Permanently
Location: https://kernel.org/
HTTP/2 200
301 https://kernel.org/A healthy permanent redirect is one 301 (or 308) whose Location is the final address, followed by a 200. On Windows, type curl.exe so PowerShell does not run its own curl alias. -I sends a HEAD request; if an application answers HEAD differently, use -o /dev/null -D - to see the headers of a real GET. More curl patterns are in curl command examples.
Without a terminal, the HTTP header and redirect checker lists every hop with its status code and Location, and can send the request as Googlebot to see what a crawler gets. In the browser, the DevTools Network tab shows the same with Preserve log ticked.
Common 301 problems
- The page never loads: two rules send the request back and forth. Chrome stops with
ERR_TOO_MANY_REDIRECTS; the causes and fixes are in ERR_TOO_MANY_REDIRECTS. - A form or API call arrives empty: a 301 turned the POST into a GET. Use 308, or point the client at the final URL.
- The redirect goes to the wrong scheme or port: nginx built an absolute URL from its own listener behind a proxy. Use
absolute_redirect offor write the full target URL yourself. - You fixed the rule, the browser still redirects: the old 301 is cached. Test in a private window.
- A script or app prints “301 Moved Permanently”: it does not follow redirects. Call the final
https://address directly, or turn on redirect following in the client, which is-Lfor curl.
Redirect rules are easiest to get right when you can read every layer that sets them. On a Linux VPS the nginx or Apache config, the .htaccess files and the application all sit on one server you control, and one curl command from that server shows what each of them answers.
Frequently asked
What does 301 Moved Permanently mean?
The server says the requested URL has a new permanent address, which it gives in the Location header. Browsers follow it automatically and may cache it, and search engines index the new URL in place of the old one. It is a redirect, not an error.
What is the difference between 301 and 302?
301 is permanent and 302 is temporary. A browser may cache a 301 even without cache headers, and search engines treat it as a strong signal to index the new URL; a 302 is not cached by default and the old URL usually stays in the index. Both allow a client to turn a POST into a GET when it follows, which 307 and 308 do not.
When should I use 308 instead of 301?
When the old URL receives POST or other requests with a body, such as forms and APIs. Clients may follow a 301 with a GET and drop the body, and curl and browsers do, while a 308 keeps the method and the body. For ordinary pages the two are equivalent: both are permanent and Google treats them the same.
How do I check if a redirect is a 301?
Run curl -sI with the URL and read the status line and the Location header, or curl -sSIL to follow the whole chain. A clean permanent redirect is a single 301 or 308 that points straight at the final address, followed by a 200.