The “too many redirects” error –
ERR_TOO_MANY_REDIRECTS
in Chrome, “The page isn’t redirecting properly” in Firefox, “Safari can’t open the page because too many redirects occurred” in Safari – means the browser asked for a URL, the server said “go here instead,” the browser asked the new URL, the server said “go here instead” again, and this repeated until the browser gave up. Most browsers stop after about 20 hops. The site is not slow, not broken, not returning an error page – it is genuinely unreachable because every URL the browser tries to visit immediately points somewhere else.
This guide covers the generic, non-WordPress causes: nginx configurations that redirect to themselves, Cloudflare SSL mode mismatches that create an HTTP/HTTPS loop, CDN and origin disagreements about canonical URLs, and
.htaccess
files fighting the web server config. If the redirect loop is specifically a WordPress one – siteurl/home mismatch, HTTPS plugin, SEO plugin – see too many redirects error in WordPress for the WordPress-specific fixes. Everything below assumes the site is a generic static site, a custom web application, or a CMS that is not WordPress.
How browsers detect redirect loops#
HTTP itself has no concept of a “loop.” Each redirect is a separate request-response pair from the server’s perspective. The browser is what tracks the sequence and decides “this has gone on too long.”
Each browser has its own limit but they cluster close together:
- Chrome/Edge: 20 redirects before showing
ERR_TOO_MANY_REDIRECTS - Firefox: 20 redirects
- Safari: 16 redirects
- curl (default): 50 with
-L, but configurable with--max-redirs
The limit is there because a browser following a redirect loop forever would just hang the tab. A hard cap lets it fail fast and tell the user something is wrong.
The important implication: the error itself tells you almost nothing about the specific problem. It only tells you that redirects happened more than 20 times. The actual loop could be between two URLs, between three URLs, or a longer cycle. The diagnostic step is always the same – trace the redirect chain manually and find the cycle.
Tracing the loop with curl#
Before changing anything, get a clear picture of what is redirecting where.
curl -Lv
follows redirects and prints every request and response header, so you can watch the loop develop:
curl -Lv https://example.com/ 2>&1 | grep -E "^(>|<) (GET|HTTP|Location)"
The
2>&1
captures both stdout and stderr (curl writes headers to stderr with
-v
), and the grep filters to just the request/response lines and any Location header (which is what makes a redirect a redirect). You will see something like:
> GET / HTTP/2
< HTTP/2 301
< Location: https://www.example.com/
> GET / HTTP/2
< HTTP/2 301
< Location: https://example.com/
> GET / HTTP/2
< HTTP/2 301
< Location: https://www.example.com/
That is the loop:
example.com
redirects to
www.example.com
, which redirects back to
example.com
, which redirects to
www.example.com
. The two layers (probably nginx and a CDN, or nginx and the application) each have their own opinion about the canonical hostname, and neither will give in.
If curl’s default 50-redirect cap is not enough (or too many), adjust it:
# Only follow 5 redirects, stop and show the state
curl -L --max-redirs 5 -v https://example.com/ 2>&1 | less
# Follow and show the complete chain more readably
curl -Ls -o /dev/null -w "%{url_effective} %{http_code}\n" -D - https://example.com/
The second form prints every response headers to stdout and shows the final URL and HTTP code at the end – very useful for “what is the final destination after all the redirects.”
Cause 1: A web server config redirects to itself#
The single most common non-WordPress cause. You wrote a redirect rule, and the rule’s target triggers the same rule again.
The classic nginx “redirect to HTTPS” loop
server {
listen 80;
listen 443 ssl;
server_name example.com;
# Meant to force HTTPS, but applies to BOTH listen directives
return 301 https://$host$request_uri;
}
The
return 301 https://$host$request_uri;
fires on both the HTTP and HTTPS listeners because they are in the same server block. A request to
https://example.com/
gets redirected to
https://example.com/
– same URL, same block, same redirect. Loop.
The fix: split the server blocks so only port 80 issues the redirect:
# Port 80 - redirect to HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
# Port 443 - serve the site
server {
listen 443 ssl;
server_name example.com;
# ... SSL config, root, location blocks
}
How to redirect HTTP to HTTPS in nginx covers the full configuration including
if
-vs-separate-block tradeoffs and how to handle the
www
variant at the same time.
The Apache
.htaccess
equivalent
# Intended: redirect HTTP to HTTPS
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
This is actually correct in Apache. The loop version is usually:
# The broken version
RewriteEngine On
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
Without the
RewriteCond %{HTTPS} !=on
guard, the rule matches on HTTPS too. A request to
https://example.com/
gets redirected to
https://example.com/
. Loop.
The fix: always guard the redirect with a condition that prevents it from matching its own target.
The IP-based redirect that matches the domain
server {
listen 443 ssl;
server_name 203.0.113.50 example.com;
return 301 https://example.com$request_uri;
}
The intent was “redirect IP-based access to the domain.” The bug is that
example.com
is also in the
server_name
, so requests to
https://example.com/
match the block and get redirected to
https://example.com/
– same URL. Loop.
The fix: separate the server blocks:
# IP-based access: redirect to the domain
server {
listen 443 ssl default_server;
server_name _;
return 301 https://example.com$request_uri;
}
# Domain: serve the site
server {
listen 443 ssl;
server_name example.com;
# ... site config
}
Cause 2: Cloudflare SSL mode mismatch#
If your origin is behind Cloudflare and you are seeing a redirect loop only when Cloudflare is active (pause Cloudflare, the site loads; re-enable, the loop returns), the cause is almost always an SSL mode mismatch.
Cloudflare has four SSL modes (SSL/TLS > Overview in the dashboard):
- Off – visitors use HTTP to reach Cloudflare
- Flexible – visitors use HTTPS to Cloudflare, Cloudflare uses HTTP to your origin
- Full – HTTPS everywhere, but Cloudflare does not validate the origin cert
- Full (strict) – HTTPS everywhere, Cloudflare validates the origin cert
The Flexible + origin-HTTPS-redirect loop
The most common configuration mistake: Flexible mode is enabled, and the origin web server has an HTTP-to-HTTPS redirect.
What happens:
- Visitor requests
https://example.com/ - Cloudflare accepts the HTTPS connection, then opens an HTTP connection to the origin
- Origin web server receives the HTTP request, sees “not HTTPS,” responds with
301 → https://example.com/ - Cloudflare receives the redirect and passes it back to the visitor
- Visitor requests
https://example.com/again - Back to step 1
Loop. The origin never sees an HTTPS request – Cloudflare is always talking to it over HTTP – so the HTTP-to-HTTPS redirect fires on every request.
The fix: switch Cloudflare to Full or Full (strict) mode. Now Cloudflare uses HTTPS to the origin, the origin’s redirect does not fire, and the page loads normally. Full (strict) is the right long-term answer because it validates the origin certificate – but you need a valid certificate on the origin first (free from Let’s Encrypt or Cloudflare’s Origin CA, which is free and lasts 15 years).
Never use Flexible mode with an HTTPS-capable origin
This comes up so often that it is worth saying explicitly: if your origin can serve HTTPS, use Full or Full (strict), never Flexible. Flexible exists only for legacy origins that literally cannot do HTTPS (old shared hosts with no SSL option). On any modern stack, Flexible creates more problems than it solves – the redirect loop is one; others include mixed-content warnings and API calls that fail because the origin sees the connection as HTTP.
The
www
redirect loop under Cloudflare
Similar pattern with the
www
subdomain. Cloudflare Page Rules or Bulk Redirects can do “example.com → www.example.com” at the edge. If the origin also redirects “example.com → www.example.com,” both redirects fire on every request and create a subtle chain – not always a visible loop but an extra hop every request. If the origin redirects in the other direction (“www → apex”), Cloudflare and the origin disagree and create a real loop.
The fix: pick one layer to handle the hostname redirect, not both. Cloudflare Page Rules at the edge is usually the cleanest because the redirect never reaches the origin. Make sure the origin’s own config does not also redirect.
Cause 3: CDN and origin disagreement on canonical URL#
Beyond Cloudflare, any CDN in front of an origin can create a loop when the CDN’s redirect rules disagree with the origin’s rules.
Common patterns:
- Fastly/BunnyCDN/KeyCDN: rule at the CDN says “redirect
/old-pathto/new-path.” Origin’s.htaccessor nginx config also has a redirect on/new-paththat sends traffic somewhere else. The two rules disagree, and depending on which runs first the chain can loop. - Varnish as reverse proxy: Varnish VCL includes a 301 redirect. The origin is set to send 301s on the same URL. Varnish caches the origin’s 301 and serves it before forwarding, which combined with Varnish’s own 301 creates a loop.
- CloudFront: a Lambda@Edge function or a CloudFront Function rewrites a URL. The origin’s S3 or ALB rules redirect the rewritten URL back to the original. Loop.
The diagnostic approach: use
curl -Lv
and watch which server is issuing each redirect. The
Via:
header and
Server:
header in the response often tell you which layer is responding.
Server: cloudflare
vs
Server: nginx
tells you which one sent the 301.
The fix: reduce the rules. Pick one layer to own URL canonicalization (CDN or origin, not both). Remove the duplicate rule from the other layer.
Cause 4: .htaccess fighting a reverse proxy or nginx#
A particularly painful case: the site is served by nginx which reverse-proxies to Apache, and both layers have redirect rules. Apache’s
.htaccess
says “redirect HTTP to HTTPS.” Nginx at the front says “redirect apex to www.” An HTTP request to
example.com
:
- Nginx redirects to
https://www.example.com/ - Nginx proxies to Apache
- Apache’s
.htaccesssees the proxied request as HTTP (because the proxy did not set theX-Forwarded-Protoheader correctly) and redirects tohttps://www.example.com/ - Nginx receives the Apache redirect and passes it to the browser
- Browser requests
https://www.example.com/ - Back to step 1
The fix: make sure the proxy passes the correct protocol header and the origin trusts it:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
On the Apache side, use mod_remoteip or trust the
X-Forwarded-Proto
header in
.htaccess
:
RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
Instead of
%{HTTPS} !=on
(which checks the direct connection and always thinks the proxied request is HTTP), check the forwarded protocol.
Cause 5: SSL certificate chain issues causing loops#
If the browser cannot verify the SSL certificate and the server keeps redirecting to HTTPS (where the untrusted cert is served), some browsers display the redirect-loop error rather than a certificate warning. This is less common but confuses people because the fix is not “stop the redirect,” it is “fix the certificate.”
Diagnostic test:
curl -v https://example.com/ 2>&1 | grep -i "ssl certificate"
If curl reports certificate verification errors but the browser shows a redirect loop, the browser is aborting the HTTPS connection silently and retrying the redirect chain. Fix the certificate (renew, install missing intermediate, check expiration) and the loop disappears.
Cause 6: Authentication or session-based redirects#
Some applications redirect based on session state:
- “User not logged in → redirect to
/login“ - “User logged in → redirect to
/dashboard“
If the session cookie gets lost between requests (browser privacy mode rejecting the cookie, domain mismatch on the cookie,
Secure
flag set but served over HTTP), both rules can fire in sequence: request
/dashboard
, no session, redirect to
/login
,
/login
sees a valid session, redirect to
/dashboard
, no session again because the cookie was never saved, redirect to
/login
, and so on.
This looks like an infinite loop even though the cookie/session logic is the actual cause. The fix is to find why the cookie is not persisting, not to change the redirect rules.
Common cookie problems:
-
Secureflag set but the site is accessed over HTTP (cookie is sent, browser rejects it on receipt) -
SameSite=Strictbreaking cross-domain OAuth flows -
Domainattribute set to a domain the browser does not match - Cookie size exceeds 4KB and the browser silently drops it
Open browser dev tools, go to the Application/Storage tab, and check whether the cookie is actually being set after the login redirect. If not, that is your loop.
Cause 7: Mobile/desktop redirects#
An older pattern: the site detects User-Agent and redirects mobile visitors to
m.example.com
while desktop visitors stay on
example.com
. If the UA detection is ambiguous or an edge case slips through, the “mobile” subdomain can redirect back to
example.com
thinking that visitor is desktop, and the loop develops.
This is less common in 2026 because most sites have abandoned the
m.
subdomain pattern in favour of responsive design, but it still exists on older sites that have not been rebuilt.
The fix: either abandon the mobile redirect entirely (responsive design covers 99% of cases) or make sure the mobile subdomain serves the mobile site without redirecting away.
Temporary fix: open a private window to confirm it is server-side#
One quick diagnostic before diving into configuration: open the site in a private/incognito window. Private windows have no cookies, no cache, no extensions.
- Loads correctly in private: the problem is browser state (cached redirect, old cookie). Clear your browser cache and cookies for the domain; the site should load.
- Same error in private: the problem is server-side. The fixes in this guide apply.
A cached 301 is a surprisingly common cause of “the site broke for me but works for everyone else.” Browsers cache permanent redirects aggressively, and a stale 301 can persist long after the server stopped issuing it. Clearing the cache for the specific domain (chrome://settings/siteData in Chrome) fixes it.
Common mistakes#
- Checking only one layer. The loop is almost always between two layers – nginx and the application, Cloudflare and the origin, CDN and the CMS. Looking only at nginx when Cloudflare is the culprit wastes hours.
- Not using
curl -Lvfirst. Guessing the redirect chain from reading configs is slower and less reliable than watching the actual requests. Always run curl first. - Fixing one rule without testing. Disable the rule, clear caches (CDN included), test in a private window. If the loop is still there, the rule you changed was not the loop’s source.
- Leaving Cloudflare Flexible SSL on. The most common single cause of loops under Cloudflare. Switch to Full or Full (strict) and keep it there.
- Relying on browser cache clears instead of fixing the server. Clearing your own cache fixes your browser, but every other visitor still sees the loop. Fix the server config.
- Trusting
X-Forwarded-Protofrom untrusted sources. If your nginx is public-facing and directly proxying to Apache, you can trust the header because only nginx is setting it. If your nginx is behind another layer (Cloudflare, AWS ALB), you need to trustX-Forwarded-Protofrom them specifically and not from arbitrary clients. Otherwise a malicious client can lie about the protocol and break your redirect logic. - Setting
Strict-Transport-Securitybefore the site works over HTTPS. HSTS tells the browser “always use HTTPS for this domain, never fall back to HTTP, for the next N seconds.” If you enable HSTS and then discover HTTPS is broken, the browser will not fall back to HTTP and will loop until the HSTS expires. Test HTTPS thoroughly before enabling HSTS. - Assuming “it worked last week” means the config is fine. DNS, upstream IPs, SSL certificates, and CDN configs drift. A config that was working can break when a certificate expires, when Cloudflare’s SSL defaults change, or when a team member modifies a Page Rule. The curl diagnostic is the baseline truth regardless of what the config “should” do.
How Hostney handles redirects#
Hostney’s nginx stack is configured to avoid the most common redirect-loop patterns out of the box. The default server blocks split HTTP and HTTPS cleanly (port 80 redirects to port 443, port 443 serves the site) so the “redirect applies to both listeners” trap does not fire. The
X-Forwarded-Proto
and
X-Forwarded-Host
headers are set correctly on all reverse-proxied requests, so applications running in Podman containers behind the front-facing nginx see the correct protocol and can apply their own redirect logic without fighting the front layer.
For users who add their own custom redirects through the control panel’s Redirects feature, the system writes them into the right nginx include file so they run at the correct phase of request processing. If a custom redirect ends up conflicting with an application-level redirect, the 301 and 302 redirect guides and the 307 and 308 guide cover the semantics that inform which redirect should own which URL pattern. For sites that have Cloudflare in front of Hostney (a common setup for traffic-heavy sites), the recommended configuration is Cloudflare in Full (strict) mode with the origin certificate issued either by Let’s Encrypt (which Hostney handles automatically) or by Cloudflare’s Origin CA – both keep the visitor-to-edge and edge-to-origin legs of the connection on HTTPS and avoid the Flexible-mode redirect loop covered in Cause 2.
When a site does run into a loop, the best first step on Hostney is the same as the guide above: pause Cloudflare at the dashboard to isolate whether the loop is origin-side or edge-side, then run
curl -Lv
against the direct origin hostname (visible in the control panel’s Hosting section) to watch which layer is issuing the redirect. For unavoidably complex setups – reverse-proxied legacy apps, multi-CDN pipelines, mixed HTTP/HTTPS legacy endpoints – Hostney’s support team can pull the live nginx config and help trace which rule in which file is creating the cycle. Sites that share an error pattern with 520 and 521 Cloudflare errors often have the same underlying edge/origin mismatch that drives both classes of problem, so fixing one frequently fixes the other.
Summary#
A redirect loop means two or more layers in the request chain disagree about what the correct URL is, and they bounce the browser back and forth until it gives up. The diagnostic is always the same: run
curl -Lv
against the URL, watch the redirect chain develop, and identify which layer is issuing each redirect. The fix depends on the cause – a web server block that redirects to itself, Cloudflare’s Flexible SSL mode fighting the origin’s HTTPS redirect, a CDN canonicalization rule conflicting with origin rules,
.htaccess
not trusting
X-Forwarded-Proto
from a proxy, an SSL certificate issue that causes some browsers to show redirect errors instead of cert warnings, or cookie/session logic that cannot maintain state between redirects. Pause Cloudflare to isolate edge vs origin. Test in a private window to isolate browser vs server. Never enable HSTS until HTTPS is verified working. And if the site is a WordPress site specifically, the WordPress version of this guide covers the siteurl/home mismatch and the plugin-level causes that do not apply to generic stacks.