HTTP Redirect Loop Fix: Diagnosing and Breaking ERR_TOO_MANY_REDIRECTS

Quick answer

💡ERR_TOO_MANY_REDIRECTS means the browser hit its redirect limit (20 for Chrome) before reaching a final destination. The most common cause is an HTTPS redirect in application code that fires even when the request arrived over HTTPS, which happens when the app sits behind a load balancer or reverse proxy that terminates TLS. Fix it by checking the X-Forwarded-Proto header before redirecting, not just the request protocol. Use curl -L -v to trace the full redirect chain.

Error symptoms

  • Chrome shows ERR_TOO_MANY_REDIRECTS and refuses to load the page
  • Browser address bar URL keeps flipping between HTTP and HTTPS variants of the same URL
  • curl -L follows redirects indefinitely or until the --max-redirs limit
  • Server access logs show rapid alternating 301 and 302 responses for the same URL
  • Page loads fine on HTTP but loops when accessed over HTTPS
  • Redirect loop only appears after deploying behind a new load balancer or CDN

Common causes

  • Application detects HTTP protocol from req.protocol instead of X-Forwarded-Proto, so it always sees HTTP even behind an HTTPS load balancer
  • WordPress FORCE_SSL_ADMIN or HTTPS redirect in wp-config.php fires in a PHP environment already behind an HTTPS-terminating proxy
  • Cloudflare SSL mode set to Flexible instead of Full, causing Cloudflare to serve HTTPS to browsers while sending HTTP to the origin, which then redirects back to HTTPS
  • mod_rewrite rules missing a condition that prevents the rule from firing on already-HTTPS requests
  • Cookie with domain mismatch creating an authentication redirect loop where the login page redirects to the app and the app redirects back to login
  • 301 redirect cached in the browser pointing to a URL that no longer exists or now redirects elsewhere

When it happens

  • After adding a load balancer or reverse proxy that terminates TLS in front of an existing application
  • After migrating a WordPress site from HTTP to HTTPS without updating Cloudflare SSL settings
  • After setting up an HTTPS-only redirect rule in both the application and the infrastructure layer
  • When a session or cookie configuration change causes the authentication flow to redirect indefinitely
  • After a 301 redirect was served for a URL that has since been reconfigured to point elsewhere

Examples and fixes

An Express app that checks req.secure to enforce HTTPS, but sits behind a load balancer that terminates TLS and forwards plain HTTP.

HTTPS redirect loop behind a load balancer

❌ Wrong

const express = require('express');
const app = express();

// Redirect HTTP to HTTPS
app.use((req, res, next) => {
  if (!req.secure) {
    return res.redirect(301, 'https://' + req.headers.host + req.url);
  }
  next();
});

app.get('/', (req, res) => {
  res.send('Hello');
});

app.listen(3000);

✅ Fixed

const express = require('express');
const app = express();

// Trust the X-Forwarded-Proto header from the load balancer
app.set('trust proxy', 1);

// Redirect HTTP to HTTPS using the forwarded protocol
app.use((req, res, next) => {
  const proto = req.headers['x-forwarded-proto'] || req.protocol;
  if (proto !== 'https') {
    return res.redirect(301, 'https://' + req.headers.host + req.url);
  }
  next();
});

app.get('/', (req, res) => {
  res.send('Hello');
});

app.listen(3000);

The broken version checks req.secure, which Express derives from the protocol of the incoming TCP connection. When a load balancer or reverse proxy terminates TLS and forwards requests as plain HTTP on port 3000, req.secure is always false — the application never sees HTTPS directly. This causes every request to trigger the redirect, including requests that the browser sent over HTTPS. The redirect goes to https://... which the load balancer receives over HTTPS, forwards as HTTP, and the application redirects again, creating the loop. The fix reads X-Forwarded-Proto, which the load balancer sets to the original protocol the browser used. Setting app.set('trust proxy', 1) tells Express to trust this header from one level of upstream proxy.

A WordPress site behind Cloudflare with Flexible SSL mode, where Cloudflare serves HTTPS to browsers but communicates with the origin over HTTP, conflicting with WordPress's HTTPS redirect.

Cloudflare Flexible SSL creating an infinite loop

❌ Wrong

# Cloudflare SSL mode: Flexible (HTTP to origin)
# wp-config.php
define('FORCE_SSL_ADMIN', true);
define('FORCE_SSL_LOGIN', true);

// WordPress receives HTTP from Cloudflare
// FORCE_SSL redirects to HTTPS
// Cloudflare receives HTTPS, sends HTTP to origin
// Loop repeats indefinitely

✅ Fixed

# Step 1: Change Cloudflare SSL mode to Full (Strict)
# Dashboard: SSL/TLS > Overview > Full (strict)

# Step 2: Add to wp-config.php before WordPress loads
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
    $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
  $_SERVER['HTTPS'] = 'on';
}

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

Cloudflare's Flexible SSL mode means Cloudflare accepts HTTPS from the browser but sends HTTP to your origin server. WordPress sitting on the origin sees HTTP, triggers its SSL redirect, sends the user back to HTTPS, which Cloudflare receives and forwards as HTTP again — an infinite loop. Changing SSL mode to Full or Full (Strict) makes Cloudflare communicate with the origin over HTTPS, breaking the loop. The wp-config.php fix sets the HTTPS server variable when the X-Forwarded-Proto header says https, telling WordPress that the request arrived over HTTPS regardless of the direct connection protocol. Setting WP_HOME and WP_SITEURL to explicit HTTPS values prevents WordPress from deriving the wrong base URL from the incoming HTTP connection.

An Apache .htaccess rule that redirects HTTP to HTTPS but fires on every request because it lacks a condition checking whether HTTPS is already active.

mod_rewrite redirect loop with missing condition

❌ Wrong

# .htaccess — loops because condition is wrong
RewriteEngine On
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

✅ Fixed

# .htaccess — checks HTTPS status before redirecting
RewriteEngine On

# Skip redirect if already HTTPS
RewriteCond %{HTTPS} off
# Also check X-Forwarded-Proto for proxy environments
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

The broken rule has no condition — it redirects every request to HTTPS unconditionally, including requests that are already on HTTPS. This creates an infinite redirect loop because the HTTPS request triggers the same rule and redirects again. The fix adds RewriteCond %{HTTPS} off to only fire the rule when HTTPS is not active on the direct connection. The second condition RewriteCond %{HTTP:X-Forwarded-Proto} !https handles the proxy case where HTTPS was already established upstream, using the X-Forwarded-Proto header to detect the original protocol. Combining both conditions with AND logic (the default in mod_rewrite) ensures the redirect only fires when neither direct HTTPS nor proxied HTTPS is in use.

Why redirect loops form and persist

A redirect loop is a cycle where request A causes a redirect to B, and B causes a redirect back to A, or to another URL that eventually cycles back to A. The browser follows each redirect automatically until it reaches its internal limit — Chrome stops after 20 redirects and shows ERR_TOO_MANY_REDIRECTS. The server's access logs tell the true story: you will see alternating status codes and URLs cycling through the same set of locations.

The most common redirect loop is the HTTPS enforcement loop behind a proxy. A load balancer or reverse proxy like nginx, AWS ALB, or Cloudflare terminates TLS and forwards decrypted HTTP requests to the application on a backend port. From the application's perspective, every request arrives over HTTP. If the application has an HTTP-to-HTTPS redirect rule that checks the direct connection protocol rather than the X-Forwarded-Proto header, it redirects every request to HTTPS. The load balancer receives the HTTPS request, forwards it as HTTP again, and the cycle repeats until the browser gives up.

Cached redirects are a secondary cause of redirect loops that is often overlooked. A 301 Moved Permanently response is cached by the browser until the cache is explicitly cleared. If you set up a redirect chain during a migration — HTTP to HTTPS, then old domain to new domain — and later remove one of the intermediate redirects, browsers that cached the 301 will continue following it. The cached redirect may now point to a URL that redirects back to the original, creating a loop that only appears for returning visitors, not in fresh browser sessions. Curl and incognito mode will not reproduce it because they do not use the cached redirect.

Authentication redirect loops are a third category where the login page redirects to the protected resource after authentication, but the protected resource redirects back to the login page because the session cookie was not set correctly. This happens when the cookie domain or path does not match the requested URL, when SameSite restrictions prevent the cookie from being sent on a redirect, or when the session store failed to save the session between the login POST and the subsequent GET redirect.

CORS interactions can also create loops. If a cross-origin request triggers an authentication redirect and the authentication server is on a different domain, the redirect does not carry the original credentials — the browser follows the redirect as an unauthenticated request, which is again redirected to authentication, creating a loop specific to cross-origin scenarios.

Tracing the full redirect chain

The fastest diagnostic tool for redirect loops is curl with verbose output. Run curl -L -v https://example.com 2>&1 | grep -E 'Location:|< HTTP' to follow all redirects and print only the status codes and Location headers. This shows the complete redirect chain without the noise of response bodies. If you see the same pair of URLs alternating, you have found your loop. Limit the redirect depth with curl --max-redirs 5 to stop curl before it exhausts the full chain and still see which URLs are involved.

In Chrome's DevTools, open the Network tab before visiting the URL. Check Preserve log at the top of the Network tab so Chrome does not clear the log when the page redirects. You will see each redirect as a separate network entry. Click on each one to see its Location header and status code. The pattern becomes clear when you see the same URLs cycling. The response headers on each redirect entry show exactly where the server is sending you.

Check the Cache-Control and Expires headers on redirect responses. If a 301 response has no Cache-Control header or has a long max-age, the browser may have cached it. Test in an incognito window with a fresh session to rule out cached redirects. If the loop does not reproduce in incognito, clear the browser cache for the affected domain and test again in a normal window.

For server-side investigation, tail your access logs while making a single request to the looping URL. Each redirect appears as a separate access log entry. You can identify the loop by seeing the same URL pair appear repeatedly in sequence. Add Cache-Control: no-store to redirect responses while debugging to prevent the browser from caching bad redirects and compounding the problem.

Use /tools/http-request-builder to send requests manually with specific headers like X-Forwarded-Proto: https and observe whether the server redirect fires. This lets you simulate what a load balancer would send to your application and confirm whether the redirect condition correctly reads the forwarded protocol. Compare the response when X-Forwarded-Proto is set to https versus http to confirm the fix is working before deploying it.

Breaking the redirect cycle by source

For HTTPS enforcement loops behind a proxy, the fix is to read X-Forwarded-Proto instead of the local connection protocol. In Express, set app.set('trust proxy', 1) and then check req.headers['x-forwarded-proto'] before redirecting. In nginx, use the scheme variable which automatically uses X-Forwarded-Proto when available. In PHP and WordPress, check $_SERVER['HTTP_X_FORWARDED_PROTO'] and set $_SERVER['HTTPS'] = 'on' when it equals 'https' so that PHP's own HTTPS checks work correctly through the proxy.

For Cloudflare-specific loops, change the SSL mode from Flexible to Full or Full (Strict) in the Cloudflare dashboard under SSL/TLS. Flexible SSL is a common default that sends HTTP to the origin, which conflicts with any application-level HTTPS enforcement. Full SSL sends HTTPS to the origin if a certificate is installed. Full (Strict) additionally verifies the certificate against a trusted CA. If your origin has a valid certificate, use Full (Strict) for the best security and no redirect loop risk.

For mod_rewrite loops, always add RewriteCond %{HTTPS} off before the HTTPS redirect rule. Add a second condition RewriteCond %{HTTP:X-Forwarded-Proto} !https for proxy environments. Both conditions together prevent the rule from firing when HTTPS is already established either directly or through a proxy. After editing .htaccess, test with curl --max-redirs 3 to confirm the redirect only fires once before reaching the final HTTPS destination.

For cached 301 redirect loops where the target URL has changed, serve the affected URL with a 302 Found instead of 301 Moved Permanently while you sort out the correct destination. Browsers do not cache 302 responses, so future requests will re-evaluate the redirect on every visit. Once the correct redirect chain is established and tested, switch back to 301. This strategy avoids forcing users to clear their browser cache manually.

For authentication redirect loops, add explicit checks that prevent the login page from redirecting to itself. After a successful POST to the login endpoint, only redirect to the intended destination if the session was successfully saved and the session ID is correctly scoped for the cookie domain. If the session is not persisted before the redirect, the GET that follows the POST will see no session and redirect back to login. Use /tools/http-request-builder to manually send the login POST, capture the Set-Cookie header, and replay a GET request with that cookie to verify the session is accepted before the redirect fires.

Redirect loops that only appear in specific environments

Multi-layer proxy architectures create redirect loops that only appear in production because local development typically has no load balancer or CDN. When a request passes through three layers — browser, Cloudflare, AWS ALB, then the application — the X-Forwarded-Proto header may be overridden or duplicated at each layer. The application might see X-Forwarded-Proto: https,http (a list) instead of a single value. Code that does a strict equality check will fail when the header contains multiple comma-separated values. Parse the first value from the list rather than checking the raw header string directly.

Trailing slash redirects interact badly with HTTPS enforcement. If nginx redirects /path to /path/ and the application redirects HTTP to HTTPS, a request to http://example.com/path can trigger two redirects: one for trailing slash and one for HTTPS. If these redirects are served separately instead of combined, some proxy configurations create a loop by applying the trailing slash redirect after the HTTPS redirect has already fired. Fix this by combining both normalizations into a single redirect using the $scheme variable in nginx.

Mobile apps that use in-app browsers or custom HTTP clients often do not handle redirects the same way as Chrome or Firefox. A redirect loop that Chrome stops at 20 iterations may cause a mobile app to loop indefinitely, consuming battery and data. When debugging mobile-specific redirect loops, use Charles Proxy or the iOS/Android network inspector to trace the exact redirect chain your app is following.

Some hosting platforms like Heroku automatically redirect HTTP to HTTPS at the platform level. If your application also has an HTTPS redirect, and the platform redirect adds the X-Forwarded-Proto header while the application's redirect condition does not check it, you get a loop at the Heroku routing layer. Check the platform documentation to confirm whether the platform already handles HTTP-to-HTTPS redirection. If it does, remove the application-level redirect entirely.

Docker compose environments where the application container runs on HTTP and an nginx container handles HTTPS termination are a common source of development-only redirect loops. If the nginx container forwards X-Forwarded-Proto but the application code is not configured to trust it, the developer sees the same HTTPS loop as in production, even though the environment is completely different from the local machine.

Mistakes that create or prolong redirect loops

Using 301 Moved Permanently before the redirect destination is confirmed stable is the most costly mistake because 301 redirects are cached by the browser and CDNs. If you redirect http://example.com to https://example.com with a 301 and later discover that https://example.com redirects back to http, every browser that cached the initial 301 will loop without any server involvement. Use 302 Found when testing redirect configurations so that the redirect is re-evaluated on each request.

Not setting Cache-Control: no-store on error pages that redirect to authentication is a common authentication loop trigger. When a protected page returns a 302 to the login page and the response has no Cache-Control header, some aggressive caches store the 302. Future requests to the protected page are served the cached 302 pointing to login, regardless of whether the user is authenticated. Set Cache-Control: no-store on all authentication-related redirects.

Relying on redirect behavior to normalize URLs instead of applying normalization before routing creates chains. If one middleware redirects to add a trailing slash, another redirects to lowercase the URL, and a third redirects HTTP to HTTPS, three sequential redirects fire before the request is handled. While not technically an infinite loop, this chain hurts performance and is fragile. Combine URL normalization into a single middleware or a single nginx rewrite rule that handles all transformations at once.

Ignoring the X-Forwarded-Proto header when configuring WordPress FORCE_SSL options is the single most common WordPress redirect loop cause. The correct pattern is to set $_SERVER['HTTPS'] = 'on' when X-Forwarded-Proto is 'https' and place this code before any WordPress core files are loaded in wp-config.php. WordPress derives the current URL scheme from this server variable, so setting it correctly prevents the HTTPS redirect from firing on already-HTTPS requests.

Not testing redirect behavior with curl before deploying to production means loop bugs reach users before they are caught. Add a curl redirect check to your deployment script: curl -L --max-redirs 5 -w '%{num_redirects}' -o /dev/null -s https://example.com. If the number of redirects exceeds 2, investigate before completing the deployment.

Preventing redirect loops in production systems

Consolidate all redirect logic into a single authoritative layer. Choose either the application layer or the infrastructure layer, not both. If nginx handles HTTPS enforcement, remove all application-level HTTPS redirects. If the application handles it, configure nginx to pass requests through without adding its own redirect rules. Duplicate redirect logic in multiple layers is the leading cause of production redirect loops.

Always use 302 Found during initial setup and testing of new redirect rules. Once the redirect chain is confirmed correct with at least 48 hours of monitoring in production, switch to 301. The 48-hour window allows CDN caches to warm with the new 302 responses, giving you time to catch any edge cases before they are cached as permanent redirects that require cache purging to fix.

Add redirect count monitoring to your application or infrastructure. Log a warning when a request triggers more than two redirects before reaching the final handler. Most legitimate redirect chains are one or two hops at most — HTTP to HTTPS is one redirect, old domain to new domain is one redirect. More than three redirects in a chain is a signal that something is wrong. Use this metric as a deployment health check rather than waiting for users to report ERR_TOO_MANY_REDIRECTS.

Document every redirect rule in your system with its purpose, source URL pattern, destination, and status code. Include the date it was added and the expected duration — some redirects are temporary (domain migrations) while others are permanent (slug changes). When redirect loops appear in production, a map of redirect rules is much faster to reason about than digging through nginx configurations, .htaccess files, application middleware, and CDN settings simultaneously.

Use /tools/http-request-builder to test redirect behavior by sending requests with specific headers like X-Forwarded-Proto and observing the response status and Location header. This is faster than setting up curl with the right flags and is useful for confirming that your application responds correctly to both forwarded and non-forwarded protocol headers. Test with X-Forwarded-Proto: http and X-Forwarded-Proto: https separately to confirm the redirect fires in exactly the right conditions. Pair this with /tools/cors-tester if authentication redirects interact with CORS configuration.

Quick fix checklist

  • Run curl -L --max-redirs 5 -v on the URL to see the full redirect chain without browser caching interference
  • Check server access logs for rapidly alternating status codes on the same URLs to confirm the loop pattern
  • Verify that HTTPS redirect logic reads X-Forwarded-Proto, not just the direct connection protocol
  • If behind Cloudflare, change SSL mode from Flexible to Full (Strict) in the SSL/TLS dashboard
  • In mod_rewrite, add RewriteCond %{HTTPS} off before the HTTPS redirect rule
  • In WordPress, add X-Forwarded-Proto detection before wp-config.php loads WordPress core files
  • Switch any 301 redirects to 302 while debugging so the browser does not cache intermediate bad redirects
  • Add Cache-Control: no-store to redirect responses during debugging to prevent caching of bad redirects

Related guides

Frequently asked questions

What causes ERR_TOO_MANY_REDIRECTS?

Chrome stops following redirects after 20 hops and shows ERR_TOO_MANY_REDIRECTS. This always indicates a loop — two or more URLs redirecting to each other in a cycle. The most common causes are an HTTPS redirect in application code that fires even when the request is already HTTPS (because the app sits behind a TLS-terminating proxy), Cloudflare Flexible SSL conflicting with server-side HTTPS enforcement, or an authentication redirect loop caused by a misconfigured session cookie.

Why does the redirect loop only happen after adding a load balancer?

Load balancers terminate TLS and forward HTTP to the backend application. The application sees every request as HTTP and fires its HTTPS redirect rule. The load balancer then receives the redirect to HTTPS, forwards it as HTTP again, and the application redirects once more. Fix it by reading the X-Forwarded-Proto header that the load balancer adds to indicate the original protocol the browser used, and only redirect when that header shows HTTP.

How do I trace a redirect loop without a browser?

Use curl -L -v https://example.com 2>&1 | grep -E 'HTTP/|Location:' to follow all redirects and print only the status codes and destination URLs. Add --max-redirs 5 to stop before curl exhausts the full chain. Each Location header shows where the server is sending you. If you see the same pair of URLs alternating, that is your loop. This approach also bypasses browser redirect caching, giving you the raw server behavior.

How do I fix a WordPress redirect loop?

Add $_SERVER['HTTPS'] = 'on' to wp-config.php when the HTTP_X_FORWARDED_PROTO server variable equals 'https'. Place this code before the WordPress require statements. If using Cloudflare, change SSL mode from Flexible to Full in the SSL/TLS dashboard. If FORCE_SSL_ADMIN is set in wp-config.php but the server is behind a proxy that sends HTTP, WordPress will always redirect to HTTPS and create a loop unless it knows the original connection was already HTTPS.

Can a cached 301 redirect cause a loop?

Yes. Browsers cache 301 responses indefinitely until the cache is cleared. If you had a 301 from URL-A to URL-B, and URL-B now redirects back to URL-A, returning visitors see a loop because their browser follows the cached 301 without re-checking the server. Test in an incognito window to rule this out. Fix by serving the affected URL with a 302 while you sort out the correct destination, then clearing the CDN cache for that URL.

What is the difference between a 301 and 302 redirect for loops?

301 Moved Permanently is cached by browsers and CDNs, sometimes for months. If you use 301 and later discover a redirect mistake, the cached redirect continues causing the loop without any server involvement. 302 Found is not cached and is re-evaluated on every request. Use 302 while testing new redirect rules. Only switch to 301 once you are confident the destination is stable and correct.

Why does my redirect loop only happen for logged-out users?

This is an authentication redirect loop. The protected page redirects unauthenticated users to the login page, but the login page then redirects back to the protected resource with a return_url parameter. If the session is not being saved before the redirect, or if the session cookie is not being sent due to domain or SameSite restrictions, the protected page sees no session again and redirects to login again, creating the loop.

How do I prevent redirect loops from happening in the first place?

Consolidate redirect logic into one layer — either the infrastructure or the application, not both. Use 302 redirects during testing and only switch to 301 when the destination is confirmed stable. Add the X-Forwarded-Proto check to any HTTPS enforcement logic so it handles proxy environments correctly. Test redirect behavior with curl before deploying, and monitor the number of redirects per request as a health metric.

All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.