Skip to content
ohsudo, Home
Networking

ERR_TOO_MANY_REDIRECTS with Nginx and Cloudflare: find the loop and break it

Fix ERR_TOO_MANY_REDIRECTS: follow the loop with curl, then fix Cloudflare Flexible SSL, HTTPS redirects behind a proxy, X-Forwarded-Proto and www redirects that fight.

Intermediate4 min readApplies to Nginx · Apache · Cloudflare · load balancers · Django · Express · WordPress · Chrome · Firefox

by OhSudo ··

Not sure this is your cause?Find it step by step: Website down →

Part of a learning path:Web servers: nginx and HTTPS in production

The browser gives up:

This page isn't working
example.com redirected you too many times.
ERR_TOO_MANY_REDIRECTS

Firefox calls it “The page isn’t redirecting properly”. Your server answers “go to A”, then A answers “go to B”, then B answers “go to A”, forever. Two parts of your setup disagree about what the right address is. Almost always it’s about http vs https or www vs no www.

  1. Visitorhttps://example.com
  2. Cloudflare (Flexible)asks your server over http://
  3. Your server301 → https://example.com(breaks here)

↻ The visitor follows the redirect to https://example.com, which starts it all again: ERR_TOO_MANY_REDIRECTS.

With Flexible SSL, Cloudflare talks to your server over plain HTTP, and your server redirects every HTTP request to HTTPS. The browser goes round until it gives up.

TL;DR

Terminal window
curl -sIL --max-redirs 8 https://example.com | grep -iE '^(HTTP|location)'

If you use Cloudflare with SSL set to Flexible and your server redirects to HTTPS, that’s the cause: set SSL/TLS to Full (strict).

1. See the loop

The browser hides it; curl shows it:

Terminal window
curl -sIL --max-redirs 8 https://example.com | grep -iE '^(HTTP|location)'
HTTP/2 301
location: https://example.com/
HTTP/2 301
location: https://example.com/
HTTP/2 301
location: https://example.com/
curl: (47) Maximum (8) redirects followed

Two things to read here:

  • A URL that redirects to itself means the server can’t see that the visitor already uses HTTPS: sections 2 and 3.
  • Two URLs taking turns, for example www and no www, means two rules disagree: section 4.

Add -A "Mozilla/5.0" if the site treats curl differently. Also try a private window: a browser remembers 301 redirects, so an old loop can stay in its cache after you’ve fixed it.

2. Cloudflare Flexible SSL

This is the classic. With Flexible, Cloudflare talks to the visitor over HTTPS, but to your server over plain HTTP:

  1. The visitor asks Cloudflare for https://example.com.
  2. Cloudflare asks your server for http://example.com.
  3. Your server, which redirects all HTTP to HTTPS, answers 301 https://example.com.
  4. The visitor asks for https://example.com. Back to step 1.

The fix: install a certificate on your server, then in Cloudflare set SSL/TLS → Overview to Full (strict). A free Cloudflare Origin Certificate or Let’s Encrypt works. The Nginx Config Generator writes the HTTPS server block.

Flexible also means the traffic between Cloudflare and your server isn’t encrypted at all, so it’s worth leaving for that reason alone. Check Rules for your own “Always Use HTTPS” and redirect rules too, in case they conflict with the server.

3. Behind a proxy or load balancer

The same loop, without Cloudflare: a load balancer or nginx ends HTTPS and talks HTTP to the app. The app thinks the request is insecure, and redirects to HTTPS. The proxy has to say how the visitor came in:

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

And the app has to trust that header. Do this only when the app really sits behind your proxy, or clients could fake it:

# Django
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
// Express
app.set('trust proxy', 1);

For WordPress, also check that siteurl and home have the right scheme and host:

Terminal window
wp option get siteurl
wp option get home

Kubernetes: with an Ingress in front, the same applies. Check the Ingress annotations that force HTTPS, and whether your app also redirects.

4. www and no www fighting

Nginx says “go to www”, and something else, like the app or a Cloudflare rule, says “go to no www”:

location: https://www.example.com/
location: https://example.com/
location: https://www.example.com/

Choose one address, and redirect in only one place. In nginx:

server {
listen 443 ssl;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
return 301 https://example.com$request_uri;
}

Then make sure the application’s own setting (ALLOWED_HOSTS, BASE_URL, WordPress home) agrees with it.

5. After a fix: test properly

Terminal window
sudo nginx -t && sudo systemctl reload nginx
curl -sIL --max-redirs 8 https://example.com | grep -iE '^(HTTP|location)'

You want one or two redirects at most, ending in 200. If curl is right and the browser still loops, clear the site’s cookies and cache, or use a private window. With Cloudflare, also purge its cache, since it can store redirects.

Shell, we have a problem!

Similar, but different: a 526 Invalid SSL certificate from Cloudflare appears when Full (strict) is on and the server’s certificate isn’t valid. 502 Bad Gateway means the proxy can’t reach the app: see Nginx 502 Bad Gateway. And a certificate error from a client is covered in unable to get local issuer certificate. If Cloudflare gets in the way of issuing a certificate, see Certbot failed to authenticate. To read a whole chain of redirects, paste the output of curl -sIL into the HTTP Headers Analyzer.

Found this useful? Share it.

Found a mistake? Let us know at contact@ohsudo.com.

Tools

Try it yourself

Articles

Related articles