How HTTP Works

Debug guide · you're seeing

  • This page isn’t working. example.com redirected you too many times. Try deleting your cookies. ERR_TOO_MANY_REDIRECTS
  • The page isn’t redirecting properly. Firefox has detected that the server is redirecting the request for this address in a way that will never complete. This problem can sometimes be caused by disabling or refusing to accept cookies.
  • Safari Can’t Open the Page. Too many redirects occurred trying to open “https://example.com/”. This might occur if you open a page that is redirected to open another page which then is redirected to open the original page.
  • curl: (47) Maximum (10) redirects followed

ERR_TOO_MANY_REDIRECTS: Find and Break the Redirect Loop

Fix ERR_TOO_MANY_REDIRECTS (redirected you too many times): Cloudflare Flexible SSL, WordPress URLs, nginx HTTPS loops and cookie loops, diagnosed with curl.

Reviewed 6 min readintermediate5 sourcesTry itMarkdown
On this page

TL;DR: Two layers disagree about the canonical URL, so each redirects to a URL the other redirects away from. Run curl -sIL --max-redirs 10 URL and read who issues each Location. The most common cause is a CDN or load balancer talking plain HTTP to an origin that insists on HTTPS (Cloudflare Flexible SSL plus an origin redirect).

What it means

The browser followed Location headers from one response to the next and gave up after 20 hops. Each response is a legitimate 3xx. Nothing is “down”. A loop looks like this:

GET https://example.com/ HTTP/2
< HTTP/2 301
< location: https://www.example.com/

GET https://www.example.com/ HTTP/2
< HTTP/2 301
< location: https://example.com/

The loop can span two URLs, as above, or many. It can also depend on state outside the URL: a cookie, a header added by a proxy, or Accept-Language. Chrome and Firefox give up after 20 redirects.

Who sent it?

Run the diagnosis from a terminal, not the browser, so nothing is cached or hidden by the address bar:

curl -sSIL --max-redirs 10 https://example.com/ \
  | grep -iE '^(HTTP/|location:|server:|cf-ray:|via:|set-cookie:|x-powered-by:)'

Read the output top to bottom. For every hop, note the status, the Location, and who answered:

HTTP/2 301
server: cloudflare
cf-ray: 8c1f2a3b4c5d6e7f-AMS
location: https://example.com/
HTTP/2 301
server: cloudflare
cf-ray: 8c1f2a3c5d6e7f80-AMS
location: https://example.com/
curl: (47) Maximum (10) redirects followed

Location pointing back to the URL you just requested is the signature of a loop that depends on a hidden input. In this example the same URL redirects to itself because the origin sees HTTP and thinks it must upgrade to HTTPS. Compare hops:

  • Server: cloudflare plus a CF-Ray on a redirect with Location equal to the request URL: the CDN is relaying the origin’s redirect, or a Cloudflare Redirect Rule or Page Rule is itself looping.
  • Server: nginx, Apache or an app header such as X-Powered-By: Express on the redirect: the origin generated it.
  • Via: 1.1 ... (CloudFront) or Server: awselb/2.0: a CloudFront behavior or an ALB redirect rule did it.

To check whether the loop depends on cookies, replay with a cookie jar. A loop that disappears with -b/-c is a cookie loop:

curl -sIL --max-redirs 10 -c jar.txt -b jar.txt https://example.com/account

To see exactly what the origin thinks it received, bypass the CDN and fake the forwarded scheme:

curl -sI http://ORIGIN_IP/ -H 'Host: example.com'
curl -sI http://ORIGIN_IP/ -H 'Host: example.com' -H 'X-Forwarded-Proto: https'

If the first returns a 301 to HTTPS and the second returns 200, the origin only needs to be told the original scheme.

Fix it, in order of likelihood

  1. HTTPS redirect behind a TLS-terminating proxy. The origin receives HTTP from a CDN or load balancer and redirects to HTTPS forever. Make the origin read X-Forwarded-Proto (or Forwarded), or serve HTTPS to the proxy.
  2. www and apex redirecting at each other. Pick one canonical host and make sure the CDN, the web server and the application agree. A redirect in the DNS provider, one in the CDN and one in the app often do not match.
  3. WordPress siteurl and home set to a different scheme or host than the one in the browser.
  4. Cookie or session loop: login redirects to the app, the app does not see the session cookie, and redirects back to login.
  5. Trailing slash or path normalization: one rule adds /, another strips it.
  6. A redirect rule matching its own destination, for example a Cloudflare Redirect Rule for http.host eq "example.com" that redirects to https://example.com/, so it also matches the HTTPS request.

Cloudflare Flexible SSL

In Flexible mode the visitor-to-Cloudflare leg is HTTPS and the Cloudflare-to-origin leg is HTTP. An origin with its own HTTP-to-HTTPS redirect (nginx return 301, Apache RewriteRule, WordPress, a plugin) will redirect every request Cloudflare makes, and the visitor ends up bouncing. The fix is in the Cloudflare dashboard under SSL/TLS, Overview: set the encryption mode to Full (strict) after installing a valid certificate (or a Cloudflare Origin CA certificate) on the origin. Full also stops the loop but does not validate the certificate. The modes are Off, Flexible, Full and Full (strict); Cloudflare’s troubleshooting page also lists Always Use HTTPS, HSTS and conflicting Redirect Rules as loop sources. Do not “fix” it by removing the origin redirect while staying on Flexible; that leaves the Cloudflare-to-origin hop unencrypted.

nginx

The common bug is a catch-all redirect that fires for traffic that already arrived over HTTPS via a proxy:

# Loops behind a proxy that talks HTTP to nginx:
server {
    listen 80;
    return 301 https://$host$request_uri;
}

If nginx itself terminates TLS, redirect only on the plain-HTTP listener and keep the HTTPS server block redirect-free. If nginx sits behind a load balancer that terminates TLS, key the redirect on the forwarded scheme:

server {
    listen 80;

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

    location / {
        proxy_pass http://app;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
    }
}

When nginx is the proxy in front of an application server that does its own HTTPS redirect, forward the scheme the client used:

location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host              $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
}

Then make the application trust that header:

// Express
app.set('trust proxy', 1) // number of proxy hops you control; req.protocol now follows X-Forwarded-Proto
# Django settings.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True

Only trust X-Forwarded-Proto if your proxy overwrites it. A header supplied by the client must never decide security behavior.

Apache

# Behind a proxy: look at the forwarded scheme, not %{HTTPS}
RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Proto} =http
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

RewriteCond %{HTTPS} off loops behind a TLS-terminating proxy because Apache always sees off.

WordPress

WordPress redirects the request to whatever siteurl and home say. If either has the wrong scheme or host, every request is redirected. Override both in wp-config.php to break out of a loop you cannot reach through the admin:

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

// Behind a proxy or load balancer that terminates TLS:
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
    $_SERVER['HTTPS'] = 'on';
}

Also disable “force SSL” options in security or caching plugins while testing; two plugins redirecting to HTTPS each is a classic.

Typical shape: /account redirects to /login when no session is found, and /login redirects to /account when it finds one. If the session cookie is set but never comes back, the app oscillates. Check in DevTools, Application, Cookies, whether the cookie is stored after the login response, and whether it is sent on the next request. Common reasons it is not:

  • Secure on a cookie set over HTTP (the browser drops it), for example when TLS terminates at a proxy and the app thinks it is on HTTP.
  • SameSite=None without Secure, which Chrome rejects.
  • A Domain attribute that does not cover the host the user is on (example.com versus www.example.com).
  • A cookie too large to send, so the server never sees it.

The cookie not sent guide walks through each of these.

Reproduce and verify

After the fix, the chain should end in a non-3xx within one or two hops:

curl -sIL --max-redirs 10 http://example.com/ | grep -iE '^(HTTP/|location:)'
HTTP/1.1 301 Moved Permanently
location: https://example.com/
HTTP/2 200

Expected redirects are fine. Anything more than one hop from a typical entry URL (http://www. to https://example.com/) is worth collapsing into a single redirect, since each hop costs a round trip. Browsers cache 301 and 308 responses aggressively, so confirm in a private window rather than the tab that showed the error. To audit chains, canonical tags and indexing signals for a public URL, use the redirect auditor.

  • 301 Moved Permanently and 302 Found: permanent redirects get cached, temporary ones do not, which changes how long a loop survives.
  • 308 Permanent Redirect behaves like 301 but preserves the request method; a POST that loops through 308 never turns into a GET.
  • X-Forwarded-Proto is the header most HTTPS-loop fixes depend on.
  • Location explains how relative and absolute redirect targets are resolved.

Frequently asked questions

How many redirects does a browser follow before ERR_TOO_MANY_REDIRECTS?

The Fetch Standard caps a redirect chain at 20, and Chrome and Firefox both stop at 20 (Firefox exposes it as network.http.redirection-limit). curl is more generous: it follows 50 redirects by default and then fails with error 47, which you can change with --max-redirs.

Why does clearing cookies sometimes fix ERR_TOO_MANY_REDIRECTS?

When an app redirects based on session state, a stale or oversized cookie can make it conclude the user is logged out on every request, so it redirects to the login page, which redirects back. Clearing the cookie resets that state. If clearing cookies fixes it only temporarily, the real bug is that the cookie is not being stored or sent back, usually because of Secure, SameSite or Domain attributes.

Why does Cloudflare cause a redirect loop with my HTTPS redirect?

With the SSL/TLS mode set to Flexible, Cloudflare connects to your origin over plain HTTP. If the origin then redirects HTTP to HTTPS, Cloudflare receives the redirect, the visitor follows it back to Cloudflare, and Cloudflare again asks the origin over HTTP. Switching to Full (strict) with a valid origin certificate stops the loop.

Can a 301 redirect loop stay broken after I fix the server?

Yes. Browsers cache 301 and 308 responses, often with no expiry, so the old broken redirect can keep replaying from the cache. Test in a private window or with curl, and clear site data in the browser to confirm the fix.

Is ERR_TOO_MANY_REDIRECTS a server error or a client error?

Neither, strictly. It is a browser-side network error with no HTTP status of its own. Every individual response in the loop is a valid 3xx; the problem is that the chain never reaches a non-redirect response. The cause is almost always server or proxy configuration.

Sources

  1. MDN: Redirections in HTTPdeveloper.mozilla.org
  2. MDN: 301 Moved Permanentlydeveloper.mozilla.org
  3. RFC 9110 Section 15.4: Redirection 3xxrfc-editor.org
  4. WHATWG Fetch Standard: HTTP-redirect fetchfetch.spec.whatwg.org
  5. Cloudflare Docs: Too many redirectsdevelopers.cloudflare.com

Keep going

Browse /search