What Is a Redirect Loop?
A redirect loop occurs when server A redirects to server B, which redirects back to server A — or when a single server redirects a URL to itself. Browsers detect this after approximately 20 hops and stop with:
ERR_TOO_MANY_REDIRECTS
This page isn't working. example.com redirected you too many times.
The user sees a blank error page. The server logs show a cascade of 301 or 302 responses. The actual misconfiguration is usually a single wrong setting.
Diagnosing with curl
Never try to debug redirect loops in a browser first — the browser cache makes it harder to see the chain. Start with curl:
# Follow all redirects and show each hop
curl -vL --max-redirs 10 https://example.com/ 2>&1 | grep -E 'Location:|< HTTP'
# Example output showing a loop:
< HTTP/1.1 301 Moved Permanently
< Location: https://example.com/
< HTTP/1.1 301 Moved Permanently
< Location: https://example.com/
# ... repeats until --max-redirs is exhausted
The -v flag shows request and response headers for each hop. The Location header in each response reveals where the loop originates.
Chrome DevTools Network Tab
If you need to see the browser's perspective:
- Open DevTools → Network tab
- Check "Preserve log" (crucial — without this the log clears on redirect)
- Navigate to the URL
- Filter by "Status: 3xx" to see only redirect responses
- Click each redirect to see the
Locationheader
Common Cause 1: HTTP→HTTPS + Proxy X-Forwarded-Proto
This is by far the most common redirect loop in production. The setup:
User → HTTPS → Nginx/Load Balancer → HTTP → Django
The proxy terminates SSL and forwards requests to Django over plain HTTP. Django sees request.scheme == 'http' and its SECURE_SSL_REDIRECT = True setting redirects to HTTPS. But the redirect goes back through the same proxy, which again forwards as HTTP. Infinite loop.
Fix: Tell Django to trust the proxy's X-Forwarded-Proto header:
# settings.py
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
With this setting, Django checks X-Forwarded-Proto: https and recognizes the request as already being HTTPS — no redirect needed.
Security note: Only set SECURE_PROXY_SSL_HEADER if you fully control the proxy. A user could forge X-Forwarded-Proto: https headers on a direct connection to bypass your HTTP→HTTPS redirect.
Common Cause 2: Cloudflare Flexible SSL
Cloudflare's Flexible SSL mode encrypts traffic between the browser and Cloudflare, but sends plain HTTP from Cloudflare to your origin server.
User → HTTPS → Cloudflare → HTTP → Your Origin
If your origin also has SECURE_SSL_REDIRECT = True (without the proxy header fix), it redirects to HTTPS. Cloudflare receives the redirect and makes a new HTTPS request to its edge — which forwards as HTTP to your origin again. Loop.
Fix: Change Cloudflare SSL/TLS mode to Full (Strict):
- Cloudflare Dashboard → SSL/TLS → Overview
- Set mode to Full (Strict)
- This requires a valid TLS certificate on your origin (Let's Encrypt works)
Full Strict encrypts the Cloudflare→origin connection, so your origin sees HTTPS and has no reason to redirect.
Common Cause 3: Double Redirect Rules
Redirect rules in multiple places can conflict. Common culprits:
- Nginx
server_nameredirect + DjangoSECURE_SSL_REDIRECT - Cloudflare Page Rule redirect + Nginx redirect
wwwredirect rule +non-wwwredirect rule both active
Principle: define redirects in one place only.
# Nginx: handle www→non-www AND http→https in a single block
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
server_name www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
# Only one canonical server block handles the app
location / { proxy_pass http://127.0.0.1:8000; }
}
With this setup, disable Django's SECURE_SSL_REDIRECT entirely — Nginx handles the HTTP→HTTPS promotion before the request reaches the application.
Framework-Specific Fixes
Django
# The complete settings for a Django app behind a trusted proxy:
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
ALLOWED_HOSTS = ['example.com', 'www.example.com']
Express (Node.js)
// Trust the first proxy in the chain (Nginx, Cloudflare)
app.set('trust proxy', 1);
// Force HTTPS middleware
app.use((req, res, next) => {
if (req.secure || req.headers['x-forwarded-proto'] === 'https') {
return next();
}
res.redirect(301, `https://${req.headers.host}${req.url}`);
});
Clearing the Browser Cache After Fix
After fixing the redirect loop, users may still be stuck if their browser has cached a 301 redirect. 301 redirects are cached forever by default. Instruct affected users to:
- Chrome: Shift+Ctrl+Delete → Clear cached images and files
- Or hard refresh: Ctrl+Shift+R (Windows) / Cmd+Shift+R (Mac)
To avoid future cache lock-in: use 302 (temporary) redirects during testing and only switch to 301 once the redirect is confirmed correct.