How HTTP Works
3xx · Redirection
< HTTP/1.1 301 Moved Permanently

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.

Reviewed 6 min readbeginner2 sourcesTry itMarkdown
Cacheable
Yes, by default
Client action
Follow Location; update stored links
Usually sent by
Origin app, reverse proxy or CDN rule
Spec
RFC 9110 §15.4.2
Often confused with
308, 302
On this page

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.

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 requires that change for browser requests. If a POST or PUT must arrive intact at the new URL, use 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, defaults and error construction.
  • 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.
  • 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 raises for 4xx/5xx, while GET follows by default.
  • curl -f: accepts 301; -L enables following. Without -L, inspect the headers using -i. The manual 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, guard and message resources.
  • Spring: WebClient.retrieve() uses WebClientResponseException for 4xx/5xx by default. RestTemplate’s default handler likewise treats those ranges as errors; a 301 alone produces neither that exception nor HttpClientErrorException.

Who sent it?

curl -sI http://example.com/old-page
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:

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.

Implementation

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

Redirect 301 /old-page /new-page

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

Express

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

Next.js

// 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'); Rails uses redirect_to '/new-page', status: :moved_permanently. 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) 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 and Apache’s Redirect 301 emit permanent moves. For nginx, run nginx -T and inspect the matching server_name, location and return lines; -T prints the loaded configuration as well as checking it.

Cloudflare: Always Use HTTPS 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 together.

AWS ALB: a matching listener redirect action with StatusCode: HTTP_301 returns 301. Inspect the rule’s substitutions 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 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 ticked bypasses the browser cache.
  • HSTS 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 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. 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

CodePermanenceMethod on followUse
301PermanentPOST may become GETPage and site moves, HTTP to HTTPS
302TemporaryPOST may become GETShort-lived moves, login bounces
307TemporaryPreservedAPI temporary redirect
308PermanentPreservedAPI endpoint moved for good

See also 301 vs 302 and 302 vs 307.

Frequently asked questions

What does 301 redirect mean?

A 301 tells browsers and search engines the resource has permanently moved to a new URL. Update your bookmarks and links.

What is the difference between 301 and 302?

301 is permanent (update links, transfer SEO). 302 is temporary (keep original URL, check back later). Use 301 for permanent URL changes.

Does 301 pass SEO value?

Google treats 301 as a strong signal that the destination should be canonical. Update links and sitemaps to the final URL and monitor the move in Search Console.

Can 301 change the request method?

Yes. RFC 9110 allows POST to become GET on a 301, and the Fetch Standard requires that change for browser requests. Use 308 when the method and body must be preserved.

Sources

  1. MDN Web Docs: 301 Moved Permanentlydeveloper.mozilla.org
  2. RFC 9110: 301 Moved Permanentlyrfc-editor.org

Keep going

Browse /search