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.
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 workingexample.com redirected you too many times.ERR_TOO_MANY_REDIRECTSFirefox 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.
- Visitorhttps://example.comHTTPS
- Cloudflare (Flexible)asks your server over http://HTTP(fails)
- 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.
TL;DR
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:
curl -sIL --max-redirs 8 https://example.com | grep -iE '^(HTTP|location)'HTTP/2 301location: https://example.com/HTTP/2 301location: https://example.com/HTTP/2 301location: https://example.com/curl: (47) Maximum (8) redirects followedTwo 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
wwwand nowww, 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:
- The visitor asks Cloudflare for
https://example.com. - Cloudflare asks your server for
http://example.com. - Your server, which redirects all HTTP to HTTPS, answers
301 https://example.com. - 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:
# DjangoSECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")// Expressapp.set('trust proxy', 1);For WordPress, also check that siteurl and home have the right scheme and host:
wp option get siteurlwp option get homeKubernetes: 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
sudo nginx -t && sudo systemctl reload nginxcurl -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.