Nginx "Too Many Redirects" Loop Fix
A redirect loop happens when Nginx (or your application behind it) keeps sending the browser back to a URL it just came from, usually because Nginx and the backend disagree about whether the connection is already HTTPS — each side thinks the other still needs to redirect.
The Problem
Visiting your site results in a browser error instead of the page loading:
ERR_TOO_MANY_REDIRECTS
This page isn't working - redirected you too many times
Checking the network tab shows the browser bouncing between the same one or two URLs repeatedly, never actually reaching a page with content.
Why It Happens
This is almost always a mismatch in HTTPS detection between two layers that both have redirect logic. The most common causes are:
- Nginx terminates SSL and forwards plain HTTP internally to the application, but doesn't tell the app the original request was HTTPS — so the app's own "redirect HTTP to HTTPS" logic keeps firing, sending the browser back to a URL Nginx then processes as HTTPS again, creating the loop
- A CDN or load balancer in front of Nginx also has its own HTTP-to-HTTPS redirect, conflicting with Nginx's redirect rule
- The application framework has "force HTTPS" middleware enabled, but isn't configured to trust the
X-Forwarded-Protoheader, so it can't tell the request already arrived securely - A caching layer (browser cache or CDN cache) has cached an old, incorrect redirect response and keeps serving it even after the underlying issue is fixed
The Fix
First, confirm Nginx is telling the backend the original protocol correctly:
location / {
proxy_pass http://backend;
proxy_set_header X-Forwarded-Proto $scheme;
}
$scheme reflects whatever protocol the client actually used to reach Nginx — this header is what allows the backend application to know the request was already secure, even though the internal connection between Nginx and the app is plain HTTP.
Next, make sure your application framework is configured to trust and read this header when deciding whether to redirect. This step varies by framework, but the underlying requirement is the same everywhere: without explicitly trusting proxy headers, most frameworks default to checking the connection's own protocol, which is always HTTP from the app's point of view when Nginx sits in front of it.
If a CDN or load balancer also redirects HTTP to HTTPS before traffic reaches Nginx, avoid having a second redundant redirect rule inside Nginx itself, since by the time traffic reaches Nginx it should already always be HTTPS in that setup. Check your CDN's dashboard for its own SSL/redirect settings rather than assuming Nginx is the only layer making that decision.
Clear cached redirects, both in the browser (hard refresh) and on any CDN layer, since a stale cached redirect response can keep the loop appearing even after the underlying misconfiguration is fixed:
curl -I https://your-domain.com
Use this to inspect the actual response headers directly, bypassing browser cache entirely, to confirm what Nginx is really sending right now.
Still Not Working?
If you're using Docker Compose with multiple reverse proxy layers (for example, Nginx in front of a framework that itself has HTTPS-redirect middleware, in front of Docker's own networking), trace the request through each layer individually with curl -v at each hop to identify exactly where the redirect is being issued from — with more than one layer capable of redirecting, it's easy to fix the wrong one first.
It's also worth checking your browser's stored HSTS state for the domain, since HTTP Strict Transport Security can cause the browser itself to rewrite HTTP requests to HTTPS before they even leave your machine, independent of anything the server does. If an earlier version of your site sent an aggressive Strict-Transport-Security header with a long max-age, the browser may keep enforcing HTTPS-only behavior for that domain even after you've since fixed the server-side redirect logic — clearing the browser's HSTS cache for that specific domain (available in most browsers' internal settings pages) removes this as a variable while debugging.
Finally, double-check for a subtle but common mistake: a return 301 https://$host$request_uri; redirect rule placed inside the same server block that's also configured to listen on port 443 for HTTPS. If that redirect rule isn't correctly scoped to only the HTTP (port 80) server block, Nginx ends up redirecting HTTPS requests to HTTPS again, creating a loop entirely on Nginx's own side without any backend involvement at all.