ERR_TOO_MANY_REDIRECTS: what causes a redirect loop and how to fix it

·7 min read

ERR_TOO_MANY_REDIRECTS means the browser followed too many redirects without ever reaching a page, almost always because the site sends the URL around in a loop. Chrome gives up after 20 redirects and shows "This page isn't working. example.com redirected you too many times." A visitor can delete that site's cookies or open it in a private window. A site owner has two rules that disagree about https, www, a trailing slash or a cookie, and curl -sSIL --max-redirs 10 will show which two.

What "redirected you too many times" means

It means the browser hit its redirect limit. A redirect is a 3xx response with a Location header, and the browser counts each one it follows. The WHATWG Fetch standard caps a request at 20 redirects, and Chromium hard-codes the same number as kMaxRedirects = 20. Chrome then shows ERR_TOO_MANY_REDIRECTS with the hint "Try deleting your cookies." Firefox calls it "The page isn't redirecting properly."

The browser doesn't look for a loop. It only counts, and a loop is the usual way to reach 20. /pricing redirects to /pricing/ because one rule wants a trailing slash, and /pricing/ redirects back because another rule strips it. Each rule is reasonable on its own. Together they never end.

How to fix too many redirects as a visitor

Clear the cookies for that one site. If that doesn't help, only the site owner can fix it.

  1. Open the page in a private or incognito window. It starts with no cookies and an empty cache.
  2. If it loads there, delete cookies for that site in your browser's site settings.
  3. If it also fails in a private window and in a second browser, the loop is on the server. Send the site owner the exact URL that failed.

When step 2 works, a stale login or region cookie was sending you in circles.

What causes a redirect loop on your site

Two layers that each think they own the same redirect. The CDN, the web server, the CMS and its plugins can all redirect, and each was usually set up by someone who didn't know about the others.

Cloudflare Flexible SSL with an origin that forces HTTPS

In Flexible mode, Cloudflare serves HTTPS to the visitor but fetches your server over plain HTTP. Your server sees HTTP and answers with a 301 to https. The browser asks Cloudflare for https, Cloudflare fetches your origin over HTTP again, and your server sends the same 301. Cloudflare's redirect loop troubleshooting page lists this case, along with Redirect Rules or Page Rules that contradict each other.

Switch the SSL/TLS encryption mode to Full (strict) and install a certificate on the origin. Deleting the origin's redirect also stops the loop, but it leaves the connection from Cloudflare to your server unencrypted.

www, non-www and trailing-slash rules that disagree

A CDN rule sends example.com to www.example.com, and an old nginx block from the previous host sends www back. Slashes cause the same fight. Next.js redirects /about/ to /about by default and the reverse when trailingSlash: true is set, so a server rewrite that strips slashes in front of that app loops on every page. Pick one host and one slash style, then keep each redirect in exactly one layer.

WordPress home and siteurl that disagree with the server

WordPress builds its links and redirects from two settings, home (Site Address) and siteurl (WordPress Address). Its canonical redirect takes the host from home, so if home says www.example.com and your server strips www, the two hand the request back and forth forever.

The scheme version appears behind a proxy or CDN that ends HTTPS. WordPress sees plain HTTP, and with FORCE_SSL_ADMIN or an HTTPS plugin on, it redirects to https, which arrives as http again. The WordPress HTTPS handbook warns about this exact loop. An http:// value left in home and siteurl after you added a certificate also puts an extra redirect in front of every link WordPress prints.

A redirect plugin fighting a server rule

Redirect plugins store rules in the database and run after the web server's rules. Someone adds a 301 from /services to /what-we-do in nginx during a migration. A year later a marketer restores the old URL with a plugin rule pointing back. When one path loops and the rest of the site works, check the plugin's list and grep the server config for that path.

A country or consent gate sets a cookie and redirects to the same URL. Browsers send it back and the page loads. Clients that don't keep cookies get the same redirect forever, and Google says its renderer clears HTTP cookies across page loads. The page works for every person and loops for Googlebot.

Login loops run the other way. /account sends you to /login because the session has expired, and /login sends you back because the cookie exists. Make both pages run the same check.

How to find a redirect loop with curl

Run curl with redirects on and a hop limit, then read the Location headers. -L follows redirects, which curl doesn't do by default. -I fetches headers only, -sS hides the progress bar but keeps errors, and --max-redirs stops the run early.

curl -sSIL --max-redirs 10 https://example.com/pricing | grep -iE '^(HTTP|location)'

A trailing-slash loop prints the same two Location values in turn until curl gives up with error 47. Trimmed, it looks like this:

HTTP/1.1 301 Moved Permanently
Location: /pricing/
HTTP/1.1 301 Moved Permanently
Location: /pricing
HTTP/1.1 301 Moved Permanently
Location: /pricing/
HTTP/1.1 301 Moved Permanently
Location: /pricing
curl: (47) Maximum (10) redirects followed

One rule adds the slash and another removes it. Search your configs for those two paths.

A Cloudflare Flexible loop looks different, because the URL redirects to itself:

HTTP/2 301
location: https://example.com/
HTTP/2 301
location: https://example.com/

An https URL that redirects to the same https URL means the server never learned the visitor arrived on https.

For loops that only crawlers hit, turn on curl's cookie engine with an empty -b and compare the two runs:

curl -sSIL --max-redirs 10 -b "" https://example.com/ | grep -iE '^(HTTP|location|set-cookie)'

If the loop disappears with -b "" and comes back without it, the redirect depends on a cookie and every crawler is stuck in it. A few apps answer HEAD, which -I sends, differently from GET. If the output looks wrong, swap -I for -o /dev/null -D - to send GET and print only the headers.

How to fix a redirect loop in nginx, Apache and wp-config.php

Give each redirect one owner and send every variant to the final URL in a single hop. If nginx handles www, delete the CDN rule. Then write the rules so the canonical URL can never match its own redirect.

nginx

# Every http request, both hosts, goes to the canonical URL in one hop
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://www.example.com$request_uri;
}

# The bare domain over https goes to www
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com;
    ssl_certificate     /etc/ssl/example.com.pem;
    ssl_certificate_key /etc/ssl/example.com.key;
    return 301 https://www.example.com$request_uri;
}

# The canonical host serves the site and has no redirect
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name www.example.com;
    ssl_certificate     /etc/ssl/example.com.pem;
    ssl_certificate_key /etc/ssl/example.com.key;
    root /var/www/example;
}

If a load balancer ends TLS and talks plain HTTP to nginx, $scheme is always http there and any redirect based on it loops. Test the header the balancer sets instead:

if ($http_x_forwarded_proto = "http") {
    return 301 https://$host$request_uri;
}

Apache .htaccess

RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
RewriteRule ^ https://www.example.com%{REQUEST_URI} [L,R=301]

Apache has the same trap. Behind a proxy that ends TLS, %{HTTPS} is always off at the origin, so the first condition matches on every request. Replace it with RewriteCond %{HTTP:X-Forwarded-Proto} !https, but only if your proxy sends that header, or the new condition loops too.

WordPress wp-config.php

If the loop locks you out of wp-admin, fix it in wp-config.php. Add these lines above the "stop editing" comment:

define( 'WP_HOME', 'https://www.example.com' );
define( 'WP_SITEURL', 'https://www.example.com' );

// Behind a proxy or load balancer that ends HTTPS
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && strpos( $_SERVER['HTTP_X_FORWARDED_PROTO'], 'https' ) !== false ) {
    $_SERVER['HTTPS'] = 'on';
}

The constants override the database values and lock both fields under Settings, General. Use the exact URL your server ends on, scheme and host included. The proxy block follows the one WordPress documents for reverse proxies.

After any of these fixes, purge your CDN cache and retest with curl or a private window. RFC 9110 lets caches store a 301 even without cache headers, so a browser or edge server can replay the old loop after the server is fixed.

Do redirect loops hurt SEO?

Yes. A URL stuck in a loop can't be crawled, so it can't be indexed. Google says its crawlers follow up to 10 redirect hops, and a loop never reaches content. Search Console files these URLs under "Redirect error" in the Page indexing report, which names a redirect loop as one of the causes.

The cookie-gated loop is the dangerous one, because your own browser never shows it. The first sign may be a growing Redirect error count in Search Console. Google's crawling docs also note that its inspection tools don't follow redirects, so use URL Inspection on the final URL and curl for the chain.

Our guide to 301 vs 302 redirects covers which status code each rebuilt rule should send. After a migration, paste your old URLs into the bulk HTTP status checker to see each one's status code and redirect target.

Trace your redirect chain hop by hop

Our Redirects, Headers & Host Checker requests your URL with GET and follows the redirects one hop at a time. It sends no cookies, so a cookie-gated loop shows up as a loop. Each hop comes back with its URL, status code, permanent or temporary, Location target and response time. When a hop points back to a URL already in the chain, it stops and reports a loop. A chain still redirecting after 10 hops is reported as truncated. It also flags HTTPS to HTTP downgrades, redirects with no Location, Refresh headers, X-Robots-Tag: noindex, hops slower than 3 seconds and any chain of two or more redirects, with a warning once it passes three.

In the same run it requests http and https on both the bare domain and www, and tells you whether all four end at one URL, which catches a www or http variant that lands somewhere else. It follows HTTP redirects only. A page that answers 200 and then redirects with JavaScript or a meta refresh ends the trace at that 200. It is not a security audit either. A run costs 10 credits.

Keep reading