What the error means

A redirect loop is two or more rules that disagree about where a page lives. Rule A sends the browser to URL B; something at B sends it back to A, or to a URL that leads back to A. The browser follows along until it hits its limit (about 20 hops in Chrome and Firefox) and gives up.

Chrome calls it ERR_TOO_MANY_REDIRECTS (“This page isn’t working … redirected you too many times”). Firefox says “The page isn’t redirecting properly”. Safari reports too many redirects.

The rules almost always live in different layers that don’t know about each other: a CDN, a load balancer, the web server config, .htaccess, the application, a CMS plugin. Each one is reasonable on its own. Together they go in circles.

If you’re visiting the site

Try two things, in this order:

  1. Open the page in a private or incognito window. If it loads there, the loop depends on something your browser has stored.
  2. Delete the cookies for that site only (in Chrome: the icon left of the address bar → Cookies and site data). Then reload.

If a private window loops too, the site is broken for everyone and only the owner can fix it. Clearing your whole browser history won’t change that.

Trace the chain yourself

Don’t debug redirect loops by staring at config files. Get the actual sequence of hops first:

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

-I sends HEAD requests. A few applications redirect differently for HEAD than for GET, so if the result looks odd, repeat with GET and print only headers:

curl -sL --max-redirs 10 -o /dev/null -D - http://example.com/ | grep -iE '^(HTTP|location)'

Run it for each variant someone might type: http:// and https://, with and without www. Loops often affect only one entry point.

The HTTP check follows up to 10 hops from outside your network and lists the status and Location of each, flagging a loop when the chain doesn’t end. Like curl, it carries no cookies, so it catches server-side loops, not ones that depend on a login state.

In the browser, DevTools → Network with “Preserve log” ticked shows every hop, including ones curl can’t see.

Read the pattern

Line the Location values up. The repeating pair tells you which kind of loop you have:

What repeatsKind of loopGo to
http:// ↔ https://, same hostHTTPS detection is wrong somewhereCauses 1 and 2
example.com ↔ www.example.comTwo layers disagree on the canonical hostCause 3
The same URL redirecting to itselfA condition that’s never satisfied, usually a proxy headerCause 2
/path ↔ /path/Trailing-slash rules fightingCause 5
/login → /account → /loginSession or cookie not stickingCause 6
/ → /ko/ → /Locale or geo redirect without a stable decisionCause 6

The usual causes

1. Cloudflare Flexible SSL plus an HTTPS redirect at the origin

This is the most common loop on Cloudflare-proxied sites, and it’s easy to see why. In Flexible mode, the visitor talks HTTPS to Cloudflare, but Cloudflare talks plain HTTP to your server. Your server, configured to redirect HTTP to HTTPS, answers with 301 Location: https://example.com/. The browser goes back to Cloudflare over HTTPS, Cloudflare fetches your server over HTTP again, and round it goes.

The tell: the chain shows the same https:// URL redirecting to itself, and the response headers on every hop include server: cloudflare.

Fix: set SSL/TLS mode to Full (strict) and give the origin a certificate. If it has none, a free Cloudflare Origin CA certificate is enough. Removing the origin’s redirect also stops the loop, but it leaves the Cloudflare-to-origin leg unencrypted. Also check for a Configuration Rule or an old Page Rule that sets Flexible for just one hostname. The modes are compared in Cloudflare SSL modes; if switching to Full produces a 525, see Cloudflare 525.

2. TLS ends at a proxy and the app doesn’t know it

Same mechanism without Cloudflare. A load balancer (AWS ALB, a Kubernetes ingress, HAProxy, another nginx) terminates HTTPS and forwards plain HTTP to the backend. The backend checks “is this request HTTPS?”, sees no, and redirects to HTTPS. Forever.

The proxy does tell the backend the original scheme, in X-Forwarded-Proto: https. The backend just isn’t reading it. Typical places:

  • Apache .htaccess: RewriteCond %{HTTPS} off is always true behind a TLS-terminating proxy. Use the header instead:

    RewriteCond %{HTTP:X-Forwarded-Proto} !https
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
  • nginx behind a proxy: $scheme is http on the backend. Redirect based on the header, or better, let only the edge do the HTTPS redirect.

    if ($http_x_forwarded_proto = "http") {
        return 301 https://$host$request_uri;
    }
  • Frameworks: tell them to trust the proxy. Express: app.set('trust proxy', 1) so req.secure reflects the header. Django: SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') alongside SECURE_SSL_REDIRECT. Laravel: configure trusted proxies. Spring Boot: server.forward-headers-strategy.

Only trust that header from your own proxy. If clients can reach the backend directly, they can send any X-Forwarded-Proto they like, which is why frameworks make you opt in.

A related mistake in plain nginx: an unconditional return 301 https://… inside a server block that listens on both 80 and 443. Every HTTPS request gets redirected to itself. Keep the redirect in a server block that listens on 80 only.

3. www and the apex disagree

One layer sends www.example.com to example.com, another sends it back. A Cloudflare redirect rule says apex; the origin’s nginx config says www. Or the registrar’s URL forwarding says one thing and the CMS another.

Fix: pick the canonical host, then make exactly one layer responsible for redirecting to it, and check that no other layer redirects the other way. Search the obvious places: CDN redirect rules, nginx -T | grep -n 'return 30', .htaccess, the CMS’s site address, and any SEO or redirect plugin.

4. WordPress siteurl and home

WordPress redirects every request to the host and scheme stored in its home and siteurl options (redirect_canonical). If those say https://www.example.com while the server forces example.com, or they say https:// while WordPress thinks the request arrived over HTTP (behind a proxy or Cloudflare Flexible), you get a loop. You often can’t reach wp-admin to fix it, because wp-admin loops too.

Check and fix from the shell:

wp option get home
wp option get siteurl
wp option update home 'https://example.com'
wp option update siteurl 'https://example.com'

Without WP-CLI, query the database (the table prefix may not be wp_):

SELECT option_name, option_value FROM wp_options WHERE option_name IN ('home','siteurl');

WP_HOME and WP_SITEURL constants in wp-config.php override the database, so look there too. If TLS terminates at a proxy, WordPress needs to be told the request was HTTPS. Add this to wp-config.php above the “stop editing” line:

if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
    $_SERVER['HTTPS'] = 'on';
}

If the loop started after installing an SSL or redirect plugin, rename its folder under wp-content/plugins to disable it and test again.

5. Trailing slashes

When a URL maps to a real directory, nginx and Apache (mod_dir) redirect /docs to /docs/. If the application or a rewrite rule strips trailing slashes, the two take turns. Frameworks and static site generators that have their own trailing-slash setting can fight the web server the same way.

Fix: decide on one form, configure the application to match, and make sure the web server isn’t adding a slash on top. For static sites, the generator’s output structure (docs/index.html vs docs.html) decides what the server does, so align that setting with your redirects.

6. Cookies, sessions and locale redirects

These loops depend on state, which is why they hit some users and not others.

  • Login that doesn’t stick. The login handler sets a session cookie and redirects to /account; /account doesn’t see the cookie and redirects to /login. Causes: a cookie marked Secure on a page served over HTTP (the browser won’t store it), a Domain attribute that doesn’t match the host (set for www.example.com, used on example.com), SameSite=None without Secure (Chromium-based browsers reject it), or a cookie that grew past the roughly 4 KB per-cookie limit browsers enforce and was silently dropped.
  • Locale or consent. / redirects to /ko/ based on Accept-Language, while /ko/ redirects to / because a language cookie says English. Any redirect that depends on a cookie needs a stable answer when the cookie is missing, and must not override what the cookie says.

To reproduce with cookies, give curl a cookie jar so it behaves like a browser:

: > /tmp/jar
curl -sIL --max-redirs 10 -b /tmp/jar -c /tmp/jar https://example.com/account \
  | grep -iE '^(HTTP|location|set-cookie)'

Look at the Set-Cookie lines: the attributes (Domain, Path, Secure, SameSite) usually reveal why the cookie isn’t sent back.

7. The browser remembers: HSTS and cached 301s

Two kinds of browser memory create loops that curl won’t reproduce.

HSTS. Once a site sends Strict-Transport-Security, the browser rewrites every http:// request for it to https:// before it leaves the machine. DevTools shows this as 307 Internal Redirect. If the server then redirects some HTTPS URL to HTTP, the browser upgrades it straight back, and loops. curl doesn’t keep HSTS state by default, so it shows a clean https → http redirect and no loop. The server-side redirect to HTTP is the bug. HSTS preload covers what HSTS commits you to.

Cached 301s. Browsers can cache permanent redirects for a long time. After you fix the server, a browser that cached the bad 301 keeps looping until its cache is cleared. Test fixes in a private window, and use 302 while you’re still experimenting. If a CDN rule caches everything, it may have cached the redirect response itself; purge it. The caching difference between the two codes is explained in 301 vs 302.

After the fix

  • Rerun the curl trace for all four entry points (http/https, with and without www). Each should end on a 200 in one or two hops, all on the same canonical URL.
  • Test in a private window, and once logged in if the site has accounts.
  • Run the HTTP check to confirm the chain from outside, and the SSL check on every hostname in the chain: each one needs a valid certificate, because the browser has to complete TLS before it can even see a redirect.
  • If you changed which layer owns HTTPS, make sure pages don’t hard-code http:// asset links; see mixed content.

Check your own domain now

Free, no sign-up. Runs the exact check this guide describes and shows what to fix.