Introduction
An Nginx redirect loop occurs when the server continuously redirects a request from HTTP to HTTPS and back again, creating an infinite cycle. The browser eventually stops with "ERR_TOO_MANY_REDIRECTS". This commonly happens when the HTTP-to-HTTPS redirect logic conflicts with backend proxy behavior or when the $scheme variable is incorrectly evaluated behind a load balancer.
Symptoms
- Browser displays "ERR_TOO_MANY_REDIRECTS" or "This page isn't redirecting properly"
curl -I http://example.comshows repeated 301/302 redirects between HTTP and HTTPS- Nginx access log shows the same URL requested multiple times with alternating 301 status codes
- The redirect loop occurs only for specific paths or only when behind a load balancer/CDN
Common Causes
- Redirect rule uses
$schemebut Nginx is behind a TLS-terminating load balancer, so$schemeis alwayshttp - Conflicting server blocks: one redirects HTTP to HTTPS, another redirects back
- Application backend issues its own HTTP-to-HTTPS redirect that conflicts with Nginx redirect
return 301 https://$host$request_uriused in a server block that is already listening on port 443- Misconfigured
ifblock withrewritecausing unexpected redirect conditions
Step-by-Step Fix
- 1.Separate HTTP and HTTPS server blocks cleanly:
- 2.```nginx
- 3.# HTTP - redirect all to HTTPS
- 4.server {
- 5.listen 80;
- 6.server_name example.com www.example.com;
- 7.return 301 https://$host$request_uri;
- 8.}
# HTTPS - serve the actual content server { listen 443 ssl; server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ```
- 1.Handle load balancer TLS termination by trusting the
X-Forwarded-Protoheader: - 2.```nginx
- 3.# When behind a TLS-terminating load balancer, port 80 receives HTTPS traffic
- 4.server {
- 5.listen 80;
- 6.server_name example.com;
# Only redirect if not already HTTPS from the load balancer if ($http_x_forwarded_proto != "https") { return 301 https://$host$request_uri; }
location / { proxy_pass http://127.0.0.1:3000; } } ```
- 1.Prevent application-level redirect conflicts by passing the correct protocol header:
- 2.```nginx
- 3.proxy_set_header X-Forwarded-Proto $scheme;
- 4.
` - 5.This tells the backend (Django, Rails, Laravel) that the original request was HTTPS, preventing it from issuing its own redirect.
- 6.Debug the redirect chain using curl:
- 7.```bash
- 8.curl -IL http://example.com 2>&1 | grep -E "< HTTP|< Location"
- 9.
` - 10.This shows each redirect step. Look for cycles like:
- 11.
` - 12.< HTTP/1.1 301
- 13.< Location: https://example.com/
- 14.< HTTP/1.1 301
- 15.< Location: http://example.com/
- 16.
` - 17.Check for conflicting rewrite rules that may cause redirects within the HTTPS block:
- 18.```nginx
- 19.# WRONG: This rewrites HTTPS to HTTP, causing a loop
- 20.rewrite ^(.*)$ http://$host$1 permanent;
# CORRECT: Remove or fix the rewrite to use HTTPS rewrite ^/old-path(.*)$ /new-path$1 permanent; ```
Prevention
- Always set
X-Forwarded-Proto $schemewhen proxying to backends that perform their own redirects - Use
return 301instead ofrewrite ... permanentfor simple redirects - it is clearer and less error-prone - Document your TLS termination architecture (Nginx handles SSL vs load balancer handles SSL) so team members configure redirects correctly
- Add a redirect test to your CI pipeline:
curl -sIL http://example.com | grep -c "301" | awk '{if ($1 > 1) exit 1}' - Use browser developer tools Network tab to trace redirect chains during staging deployment