net::ERR_TOO_MANY_REDIRECTS

⚡ Fastest fix

Just enabled HTTPS or moved behind a CDN/proxy? The #1 cause is an SSL mode mismatch between the proxy and the origin. If it's Cloudflare specifically, switch SSL/TLS mode from Flexible to Full (or Full (strict)) — that alone resolves the majority of cases within a minute of propagation.

What you're seeing

This page isn't working
example.com redirected you too many times.
Try clearing your cookies.
ERR_TOO_MANY_REDIRECTS

Chrome's own suggestion in the error page — "try clearing your cookies" — is a decent generic hint but is far from the only cause, and often not even the most common one. The browser stops following redirects after roughly 20 hops specifically to abort loops like this rather than hanging indefinitely; the exact cap is an implementation detail (Firefox and Safari use similar but not identical limits), but the underlying meaning is the same across browsers: it visited the same effective destination over and over with no terminating 200 OK in sight.

30-second triage

Fix 1 — SSL/proxy mode mismatch (the most common cause by far)

When: the site is fronted by Cloudflare, another CDN, or an nginx/Apache reverse proxy, and this started right after enabling or changing HTTPS.

# trace exactly what each hop is doing:
curl -IL https://example.com/
# look for a repeating pattern like:
#   HTTP/2 301 → location: https://example.com/
#   HTTP/2 301 → location: https://example.com/   (same URL again — this is the loop)

# on the origin server (nginx example) — a forced-HTTPS redirect that
# fires even when the connection already arrived as HTTPS from the proxy:
server {
    listen 80;
    return 301 https://$host$request_uri;   # fine on its own —
}                                            # but if Cloudflare's "Flexible"
                                             # mode talks to this origin over
                                             # port 80 regardless, this fires
                                             # every single time and loops

Cloudflare's "Flexible" SSL mode terminates HTTPS at Cloudflare's edge and then connects to your origin server over plain HTTP, regardless of what the visitor's browser used. If your origin also has a rule that redirects any HTTP request to HTTPS (a very standard, otherwise-correct security practice), the two configurations fight each other: origin says "this came in as HTTP, redirect to HTTPS," Cloudflare serves that redirect back to the browser over HTTPS, the browser requests again, Cloudflare — still in Flexible mode — forwards to the origin over HTTP again, and the origin redirects again. Neither side is lying or misconfigured in isolation; the loop is an emergent property of two independently reasonable rules that assume the opposite thing about what protocol the other side is using. Switching Cloudflare to "Full" or "Full (strict)" tells it to also speak HTTPS to the origin, which breaks the cycle immediately.

Fix 2 — corrupted or oversized session cookie

When: the site loads fine in a fresh incognito window but loops only in your regular profile, or only once logged in.

# quick isolation test — does an incognito window (no cookies) work?
# Chrome: Ctrl+Shift+N (Cmd+Shift+N on macOS), then load the same URL

# if incognito works, clear just this site's storage:
# click the padlock in the address bar → Cookies and site data → Delete

Some auth systems store a session token or a serialized user-state blob in a cookie, and treat any cookie value that fails to parse or verify as "not logged in" rather than "malformed" — issuing a login redirect. If the login flow then re-sets the very same broken cookie (or a middleware re-attaches it from a stale value before the response is sent), every subsequent request repeats the same failed check, producing a loop that looks identical to a server misconfiguration but is entirely client-side state. This is also common after a cookie grows past a server's header-size limit (nginx defaults to 4-8KB for request headers) — the server rejects the oversized request outright, the client/proxy's error handling turns that into another redirect, and the cycle continues.

Fix 3 — your own auth middleware redirects into itself

When: you wrote the authentication logic (Express middleware, Next.js middleware, a framework's route guard) and the loop is reproducible for every unauthenticated visitor.

// ❌ the matcher includes /login itself, so the login page
//    also gets redirected to /login, forever:
export const config = { matcher: ["/((?!_next|favicon.ico).*)"] };

export function middleware(req) {
  const isAuthed = Boolean(req.cookies.get("session"));
  if (!isAuthed) {
    return NextResponse.redirect(new URL("/login", req.url));
  }
}

// ✅ explicitly exclude the login route (and any public routes)
//    from the protected matcher:
export const config = {
  matcher: ["/((?!_next|favicon.ico|login|api/auth).*)"],
};

This is a textbook off-by-one in route protection: the middleware's matcher pattern is written broadly (often deliberately, to catch every route by default) and the developer forgets that the redirect *target* also needs to be carved out of that same pattern. Without the exclusion, an unauthenticated visit to any protected route redirects to /login, which itself matches the "protected route" pattern, fails the same auth check, and redirects to /login again — a self-inflicted loop that has nothing to do with the browser, cookies, or any upstream proxy at all. The fix is always to make the set of publicly-accessible routes (login, signup, password reset, static assets) an explicit exception to whatever pattern drives the redirect.

Fix 4 — WordPress/CMS siteurl mismatch after a domain or protocol change

When: a WordPress (or similar CMS) site started looping right after migrating domains, moving to HTTPS, or restoring a database backup.

-- check what WordPress itself believes its own URL is:
SELECT option_name, option_value FROM wp_options
WHERE option_name IN ('siteurl', 'home');

-- if either still points at the old http:// or old domain, update both:
UPDATE wp_options SET option_value = 'https://example.com' WHERE option_name = 'siteurl';
UPDATE wp_options SET option_value = 'https://example.com' WHERE option_name = 'home';

WordPress stores its own canonical base URL in the database (not just in server config), and uses it to build the redirects behind its own "force HTTPS" or canonical-domain features. If a database backup gets restored onto a new domain, or HTTPS is enabled at the server level without updating these two rows, WordPress redirects every request toward the URL it still thinks is canonical — which, if that URL now itself triggers a further redirect (e.g. the server also force-redirects http→https and the stored value is still http), produces exactly this loop. This is a distinct failure mode from the proxy-mismatch case in Fix 1: here the loop originates entirely from application-layer configuration stored in the database, not from any proxy/origin protocol disagreement.

Why this happens: redirects have no memory of their own history

the loop repeats until the browser gives up Browser (you) Cloudflare edge · Flexible SSL Origin server HTTPS HTTP 301 → https 301 → https ~20 hops, then aborts ERR_TOO_MANY_REDIRECTS no terminating 200 response

Neither side is wrong alone — they disagree about which protocol the origin is actually reached on. Switching Cloudflare to Full / Full (strict) breaks the cycle.

HTTP redirects are stateless by design — each 3xx response is just an instruction ("go fetch this other URL instead"), and the server issuing it has no built-in mechanism to know that the browser has already been sent in a circle. The browser is the only party positioned to detect a loop, and it does so heuristically: by tracking the sequence of URLs (or, more precisely, request signatures) it has followed in the current navigation and aborting once that sequence exceeds a hard cap, rather than by understanding *why* the loop is happening. This is why the fix is never on the browser side — the browser is correctly reporting a real, infinite server-side (or proxy-side) redirect chain, and the actual defect always lives in whichever layer is issuing a redirect based on incomplete or contradictory information: a proxy that doesn't know what protocol its origin actually wants, a cookie that fails validation in a way that re-triggers the same redirect, or a route matcher that fails to exclude its own destination.

Common causes at a glance

SymptomRoot causeFix
started right after enabling HTTPS/CDNproxy talks HTTP to origin, origin forces HTTPSset proxy to Full/Full(strict) SSL mode
works in incognito, loops in normal profilecorrupted/oversized session cookieclear site-scoped cookies
every unauthenticated visit loopsauth middleware redirects into its own protected patternexclude login/public routes from the matcher
WordPress site after migration/backup restorestale siteurl/home option valuesupdate both rows in wp_options

Debugging checklist

Frequently Asked Questions

What does net::ERR_TOO_MANY_REDIRECTS mean?

The browser followed a chain of HTTP redirect responses (301, 302, 303, 307, 308) that kept pointing back to a URL it had already visited, without ever reaching a page that returns 200. Browsers cap the number of redirects they'll follow (Chrome stops at around 20) specifically to detect and abort these loops rather than hanging forever.

Why does this happen right after enabling HTTPS or putting a site behind Cloudflare?

This is the single most common cause: the origin server redirects HTTP to HTTPS, but the reverse proxy or CDN in front of it (frequently Cloudflare in "Flexible" SSL mode) talks to the origin over plain HTTP. The origin sees an HTTP request, issues its HTTPS redirect, the proxy serves that redirect back over HTTPS to the browser, the browser requests again, the proxy forwards it to the origin over HTTP again — and the loop repeats indefinitely because neither side is aware of what protocol the other end is actually using.

Can browser cookies cause a redirect loop?

Yes. A stale, corrupted, or oversized cookie tied to a login/session system can cause the server to treat every single request as unauthenticated — issuing a login redirect, receiving the request again with the same broken cookie attached, and redirecting to login again forever. This is diagnosable by testing the same URL in a fresh incognito/private window with no cookies at all.

How do I see the actual chain of redirects, not just the final error?

Open DevTools Network tab, check "Disable cache", reload, and look at the sequence of requests to the same-looking URL — each row shows its status code (301/302/etc.) and its Location response header, letting you see exactly where each hop is being sent before the browser gives up.

Why would my own Express/Next.js auth middleware cause an infinite redirect?

A common bug is an authentication check that redirects unauthenticated users to a login page, but the login page itself is inside the same route group the middleware protects — so the redirect target also fails the auth check and gets redirected again, forever. The fix is always to explicitly exclude the login/auth routes themselves from the protected-route matcher.

Does clearing cookies for just one site actually fix it, or do I need to clear everything?

Site-scoped clearing is sufficient and preferable — in Chrome, click the padlock/info icon in the address bar → Cookies and site data → Delete, or use chrome://settings/content/all?searchSubtext=<domain> to remove storage for exactly that origin without wiping cookies for every other site you're logged into.

More network & server errors

Browse the full reference for network, redirect, and server-configuration errors — exact message, cause, and fix.

All Error References net::ERR_CERT_AUTHORITY_INVALID HTTP Status Codes
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.