You point your domain to a CDN, toggle SSL on, and your browser instantly bombs out with ERR_TOO_MANY_REDIRECTS. What’s happening is straightforward: your visitor hits the CDN on HTTPS, the CDN talks to Nginx on port 80 over plain HTTP, Nginx fires back an unconditional 301 redirect to HTTPS, and the cycle loops 20 times until Chrome or Firefox gives up.
I’ve run into this loop countless times across production VPS setups. Let’s trace the loop with curl and fix your Nginx configs, edge proxy headers, and application-level checks so traffic resolves properly.

How the CDN Redirect Loop Happens
To fix the loop, you need to see where the handshake breaks down. Here is the path a request takes through your proxy layer:
- Visitor → CDN: The user connects over HTTPS on port 443.
- CDN → Nginx Origin: If the CDN is set to “Flexible” mode or configured with an HTTP origin protocol, it connects to your server on plain port 80.
- Nginx Catch-All: Your Nginx config sees traffic on port 80 and runs an unconditional redirect:
return 301 https://$host$request_uri;. - The Infinite Loop: Nginx returns that 301 to the CDN. The CDN sends it to the browser. The browser requests HTTPS again, the CDN hits port 80 on Nginx again, and you’re stuck in a loop.
Even without an explicit Nginx redirect, backends like WordPress, Node.js, or Laravel will trigger their own internal redirects if they don’t know the visitor originally connected via HTTPS. We need to fix both the edge SSL mode and how Nginx handles proxy headers.
Step 1: Trace the Redirect Chain with cURL
Before touching config files, inspect the redirect chain with curl against both your public domain and your origin IP. This tells you immediately whether the 301 is coming from the CDN edge, Nginx, or an upstream PHP process.
Run this in your terminal to see the header chain:
curl -IL https://example.comIf you see 15 to 20 consecutive 301 or 302 responses pointing to the same URL, you’re looping. Now bypass the CDN completely and hit your origin IP directly:
curl -I -H "Host: example.com" http://YOUR_ORIGIN_IP/If hitting port 80 on your origin returns a 301 Moved Permanently to https://example.com/, your origin is demanding HTTPS without checking if the CDN already terminated SSL for the client.
Step 2: Match Your CDN SSL Encryption Mode
The most common cause of this error is using an edge CDN (Cloudflare, Fastly, etc.) set to “Flexible” SSL while your server forces HTTPS. In Flexible mode, the CDN accepts HTTPS from the visitor but talks plain HTTP to your server.
Fix this at the edge by switching your CDN encryption mode from Flexible to Full or Full (Strict):
- Flexible: Browser → (HTTPS) → CDN → (HTTP) → Nginx. (Avoid this).
- Full: Browser → (HTTPS) → CDN → (HTTPS) → Nginx. Requires a valid or self-signed cert on Nginx.
- Full (Strict): Browser → (HTTPS) → CDN → (HTTPS) → Nginx. Requires a valid, trusted CA certificate on Nginx (like Let’s Encrypt).
If you haven’t set up an origin certificate yet, generate one using our guide on Let’s Encrypt Wildcard SSL with Certbot on Nginx. Once Nginx listens on port 443 with a valid certificate, set your CDN origin protocol to HTTPS.
Step 3: Forward and Trust X-Forwarded-Proto in Nginx
When a CDN proxies a request, it sends the visitor’s original protocol in the X-Forwarded-Proto header. By default, Nginx doesn’t inspect or trust this header unless you tell it to.
Open your primary Nginx config (usually /etc/nginx/nginx.conf) and add a map block inside the http {} context. This creates a $redirect_to_https variable that only triggers if the original request was actual plain HTTP:
http { # Check forwarded protocol from CDN map $http_x_forwarded_proto $redirect_to_https { default 0; "http" 1; "" $is_http_without_proxy; } map $scheme $is_http_without_proxy { default 0; "http" 1; }
}Next, edit your virtual host server block (e.g., /etc/nginx/sites-available/example.com). Instead of a blind redirect on port 80, only redirect when the CDN confirms the original visitor was on HTTP:
server { listen 80; listen [::]:80; server_name example.com www.example.com; if ($redirect_to_https = 1) { return 301 https://$host$request_uri; } location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; }
}To keep visitors from bypassing the CDN and spoofing headers directly against your origin, lock down incoming traffic. Check our guide on how to restrict Nginx access to CDN Origin Shield IPs.
Step 4: Fix FastCGI and WordPress HTTPS Detection
If you’re running WordPress or PHP-FPM behind Nginx, PHP checks the HTTPS environment variable. When Nginx receives a proxied request on port 80, $https is empty. PHP assumes the connection is insecure and issues its own 301 redirect back to https://.
You can fix this in Nginx by dynamically setting the HTTPS FastCGI parameter based on the forwarded proto header.
Add this inside your PHP location block or your fastcgi_params file:
# Map incoming forwarded proto to FastCGI HTTPS flag
map $http_x_forwarded_proto $fastcgi_https_param { default off; https on;
} server { listen 80; listen 443 ssl http2; server_name example.com; location ~ .php$ { include fastcgi_params; fastcgi_pass unix:/run/php/php8.2-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; # : Tells PHP that the connection is HTTPS fastcgi_param HTTPS $fastcgi_https_param; }
}If you’re on WordPress and can’t edit global Nginx maps, you can configure WordPress to trust reverse proxy headers directly. Open wp-config.php and add this above the /* That's all, stop editing! */ line:
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') { $_SERVER['HTTPS'] = 'on';
}This stops WordPress from generating redundant internal canonical redirects when the front-end connection is already secure. If you hit asset issues after doing this, see our walkthrough on how to fix WordPress mixed content warnings after SSL.
Step 5: Test and Reload Nginx
Always validate your Nginx configuration syntax before reloading the daemon. A typo or stray semicolon will crash your server.
Run the configuration test:
sudo nginx -tIf it returns syntax is ok and test is successful, reload Nginx:
sudo systemctl reload nginxStep 6: Purge CDN Cache and Verify
Here’s a common trap: you fix the Nginx config, test in Chrome, and still see ERR_TOO_MANY_REDIRECTS. That happens because 301 redirects are cached aggressively by both your browser and the CDN edge.
Here is how to get a clean verification:
- Purge the CDN Cache: Log in to your CDN dashboard and run a full cache purge. Take a look at our guide on configuring immutable cache headers and cache busting in Nginx to avoid sticky redirect caches down the line.
- Use Incognito or Clear Browser Cache: Open a fresh private window to avoid cached HSTS/301 responses.
- Verify with cURL: Test the URL directly from your command line:
curl -I https://example.com/A clean setup returns a single HTTP/2 200 (or HTTP/1.1 200 OK) on the first request without bouncing through intermediate redirects.
Edge Cases and Common Gotchas
If you’re still seeing redirect loops after applying these changes, check these edge cases:
- Cloudflare Page Rules / Edge Rules: Make sure you don’t have conflicting Page Rules where “Always Use HTTPS” clashes with an HTTP forwarding rule. See the Cloudflare SSL redirect troubleshooting documentation for details on rule order.
- Nested Reverse Proxies: If Nginx proxies to Docker or Node.js, ensure Nginx passes
proxy_set_header X-Forwarded-Proto $scheme;or carries through the edge header properly as described in the Nginx proxy module documentation. - Mismatched Site URLs: In WordPress (
siteurl/homein the database) or Laravel (APP_URLin.env), make sure the URL begins withhttps://. If the application thinks its canonical URL is HTTP, it will loop indefinitely.
Frequently Asked Questions
Why does ERR_TOO_MANY_REDIRECTS only show up after enabling a CDN?
CDNs terminate SSL at their edge servers. When the CDN connects to your origin server over plain HTTP (port 80) while your origin expects HTTPS, Nginx redirects back to HTTPS. The CDN relays that redirect to the browser, creating an infinite loop.
Can I just disable SSL on my Nginx origin server?
You can run plain HTTP on your origin if your CDN SSL mode is set to Flexible, but your origin-to-edge traffic will travel unencrypted over the public internet. The safer, standard approach is to put a free cert on Nginx and run Full or Full (Strict) SSL.
How do I confirm Nginx is receiving X-Forwarded-Proto?
You can temporarily add add_header X-Debug-Proto $http_x_forwarded_proto; inside your Nginx location block, reload, and run curl -I https://example.com to inspect what header value reaches your server.
Why is the error still showing after I fixed Nginx?
301 redirects are cached permanently by browsers and CDN edge nodes. You must purge your CDN cache and test using an incognito window or curl -IL to bypass stale local caches.

