# HTTP 301 Moved Permanently: Permanent Redirect

> Learn what 301 redirect means, when to use it vs 302, and how to implement permanent redirects for SEO and URL changes.

Source: https://howhttpworks.com/status-codes/301
Last reviewed: 2026-10-05

> **TL;DR:** 301 Moved Permanently means the requested URL has permanently moved to another URL. Update your client or links to `Location`; if the destination is wrong, fix the redirect rule and check with browser caching disabled.

## What it means

The server is saying the target URL has a new permanent address. Clients should use the new URL for future requests. For Google, a permanent redirect is a canonicalization signal; see the SEO section below. The HTTP exchanges on this page are illustrative.

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

HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-page
Content-Length: 0
```

Two spec details that matter in practice (RFC 9110 section 15.4.2):

- A 301 is heuristically cacheable unless the method or explicit cache controls say otherwise. A cache can reuse it without contacting the server. Set an explicit freshness policy when you need a predictable lifetime; the status alone sets no fixed duration.
- A user agent may change POST to GET on 301. The [Fetch redirect algorithm](https://fetch.spec.whatwg.org/#http-redirect-fetch) requires that change for browser requests. If a POST or PUT must arrive intact at the new URL, use [308](https://howhttpworks.com/status-codes/308).

## What you see in your client

An HTTP 301 is a redirect response. Automatic following can hide it behind the destination's status, so inspect the first response when your browser and script appear to disagree.

- **Axios:** in Node's HTTP adapter, `maxRedirects: 0` exposes the redirect. With the default success predicate, it rejects with `AxiosError: Request failed with status code 301`. Accept the code in `validateStatus` to inspect `response.headers.location`. The behavior comes from the [HTTP adapter](https://github.com/axios/axios/blob/v1.x/lib/adapters/http.js), [defaults](https://github.com/axios/axios/blob/v1.x/lib/defaults/index.js) and [error construction](https://github.com/axios/axios/blob/v1.x/lib/core/settle.js).
- **Browser fetch:** follows redirects by default and returns the final response. With `redirect: 'manual'`, a browser returns `type: 'opaqueredirect'`, `status: 0` and `ok: false`, with hidden headers. A directly exposed 301 would also have `ok: false`, since only 200–299 passes. Use the Network panel or curl for the actual `Location`; these rules are in the [Fetch Standard](https://fetch.spec.whatwg.org/#concept-request-redirect-mode).
- **Python requests:** `requests.get(url, allow_redirects=False)` exposes 301; `raise_for_status()` returns normally and `response.ok` is `True`. Inspect `response.headers['Location']`. Its [error guard](https://requests.readthedocs.io/en/latest/_modules/requests/models/#Response.raise_for_status) raises for 4xx/5xx, while [GET follows by default](https://github.com/psf/requests/blob/main/src/requests/sessions.py).
- **curl `-f`:** accepts 301; `-L` enables following. Without `-L`, inspect the headers using `-i`. The [manual](https://curl.se/docs/manpage.html#-f) reserves failure code 22 for HTTP responses of 400 or greater.
- **.NET:** set `HttpClientHandler.AllowAutoRedirect = false` to inspect the first response. Calling `EnsureSuccessStatusCode()` then throws `HttpRequestException`; with English resources and reason phrase `Moved Permanently`, the message is `Response status code does not indicate success: 301 (Moved Permanently).` See the [redirect setting](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler.allowautoredirect), [guard](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs) and [message resources](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/Resources/Strings.resx).
- **Spring:** [`WebClient.retrieve()`](https://docs.spring.io/spring-framework/reference/web/webflux-webclient/client-retrieve.html) uses `WebClientResponseException` for 4xx/5xx by default. [`RestTemplate`'s default handler](https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java) likewise treats those ranges as errors; a 301 alone produces neither that exception nor `HttpClientErrorException`.

## Who sent it?

```bash
curl -sI http://example.com/old-page
```

```http
HTTP/1.1 301 Moved Permanently
Server: nginx
Location: https://example.com/old-page
```

Use response headers such as `Server` as clues, then correlate the request with edge rules and origin logs. A CDN can forward an origin redirect or emit its own; identify the matching rule before editing your app. Follow the full chain and count hops:

```bash
curl -sIL -o /dev/null -w '%{num_redirects} hops, final %{url_effective} (%{http_code})\n' http://example.com/old-page
```

For a visual chain check use the [redirect audit tool](https://howhttpworks.com/tools/redirect-audit).

## Implementation

### nginx

```nginx
location = /old-page {
    return 301 /new-page;
}

# HTTP to HTTPS
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}
```

### Apache

```apache
Redirect 301 /old-page /new-page

RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
```

### Express

```javascript
// res.redirect defaults to 302, so pass 301 explicitly
app.get('/old-page', (req, res) => res.redirect(301, '/new-page'))
```

### Next.js

```javascript
// next.config.js: permanent: true sends 308, not 301
module.exports = {
  async redirects() {
    return [{ source: '/old-page', destination: '/new-page', permanent: true }]
  }
}
```

Next.js `permanent: true` emits 308. Use middleware or a platform rule if you specifically need a 301.

## Common causes by stack

**Express / Rails:** Express requires [`res.redirect(301, '/new-page')`](https://expressjs.com/en/5x/api/response/#res.redirect); Rails uses [`redirect_to '/new-page', status: :moved_permanently`](https://api.rubyonrails.org/classes/ActionController/Redirecting.html). Both default to 302 when you omit the status. Check that argument before diagnosing a permanent redirect as a framework default.

**Django:** [`redirect('/new-page/', permanent=True)`](https://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#redirect) emits 301 with the default `preserve_request=False`. Inspect the view's flags; adding `preserve_request=True` selects 308 instead.

**nginx / Apache:** nginx's [`return 301`](https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#return) and Apache's [`Redirect 301`](https://httpd.apache.org/docs/2.4/mod/mod_alias.html#redirect) emit permanent moves. For nginx, run `nginx -T` and inspect the matching `server_name`, `location` and `return` lines; [`-T`](https://nginx.org/en/docs/switches.html) prints the loaded configuration as well as checking it.

**Cloudflare:** [Always Use HTTPS](https://developers.cloudflare.com/api/resources/zones/subresources/settings/) answers HTTP requests with 301 to the equivalent HTTPS URL. Check that setting and Redirect Rules when a request redirects before appearing in origin logs. For an HTTPS loop, inspect [SSL mode and origin redirect rules](https://developers.cloudflare.com/ssl/troubleshooting/too-many-redirects/) together.

**AWS ALB:** a matching listener `redirect` action with `StatusCode: HTTP_301` returns 301. Inspect the [rule's substitutions](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/rule-action-types.html#redirect-actions) for host, protocol, port and path; a stale hostname there can redirect every request to an obsolete site.

**Kubernetes ingress-nginx:** [`nginx.ingress.kubernetes.io/permanent-redirect`](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#permanent-redirect) selects 301 unless `permanent-redirect-code` overrides it. Inspect the Ingress metadata when the backend never sees the request. Its automatic TLS redirect defaults to 308, so distinguish that rule from a permanent-redirect annotation.

## The cache trap

Because 301 is cached, a mistake sticks. Typical incidents: a redirect to a staging host that was later removed, an `http` to `https` redirect added before the certificate worked, or a loop that persists in the browser after the server is fixed (`ERR_TOO_MANY_REDIRECTS`). Fixes:

- Test new rules with 302 and promote to 301 once verified.
- Send `Cache-Control: max-age=3600` on a 301 you are unsure about, so that cached redirect becomes stale after an hour, accounting for its current age. A new request after expiry can fetch the corrected rule.
- To reproduce a "fixed on the server, still broken in my browser" report, run `curl -sI` (curl does not cache) and compare with a new private browser session. In Chrome, DevTools Network with [Disable cache](https://developer.chrome.com/docs/devtools/network/reference/#disable-cache) ticked bypasses the browser cache.
- [HSTS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security) is a separate policy: while it applies, the browser upgrades HTTP URLs to HTTPS before sending the request. Diagnose that browser upgrade separately from a server-issued 301.

## SEO

- Google treats 301 and 308 as a [strong signal](https://developers.google.com/search/docs/crawling-indexing/301-redirects) that the destination should be canonical. Use the code that matches the permanence of the move.
- Google recommends keeping migration redirects for [at least a year](https://developers.google.com/search/docs/crawling-indexing/site-move-with-url-changes). Keep useful redirects longer while old links remain in circulation.
- Update internal links and sitemaps to the final URL instead of relying on the redirect.
- Collapse chains (A to B to C) into a single hop; Googlebot follows a limited number of hops and every hop adds latency.

## 301 vs the other redirects

| Code | Permanence | Method on follow                   | Use                               |
| ---- | ---------- | ---------------------------------- | --------------------------------- |
| 301  | Permanent  | POST may become GET                | Page and site moves, HTTP to HTTPS |
| 302  | Temporary  | POST may become GET                | Short-lived moves, login bounces  |
| 307  | Temporary  | Preserved                          | API temporary redirect            |
| 308  | Permanent  | Preserved                          | API endpoint moved for good       |

See also [301 vs 302](https://howhttpworks.com/compare/301-vs-302) and [302 vs 307](https://howhttpworks.com/compare/302-vs-307).

## Related

- [302 Found](https://howhttpworks.com/status-codes/302)
- [307 Temporary Redirect](https://howhttpworks.com/status-codes/307)
- [308 Permanent Redirect](https://howhttpworks.com/status-codes/308)
- [Location header](https://howhttpworks.com/headers/location)
- [301 vs 308](https://howhttpworks.com/compare/301-vs-308): when a redirected POST must stay a POST.
