How to Fix "Nginx Rewrite Rule Causing Infinite Redirect Loop"
Quick answer
A rewrite rule meant to redirect a URL ends up redirecting the browser back to itself over and over, until the browser gives up with a "too many redirects"...
A rewrite rule meant to redirect a URL ends up redirecting the browser back to itself over and over, until the browser gives up with a "too many redirects" error. This happens because the rewritten URL still matches the same rule that triggered the rewrite in the first place, sending the request back through the same logic endlessly.
The Problem
The browser reports a redirect loop instead of loading the page:
$ curl -IL https://yourapp.com/old-page
HTTP/1.1 301 Moved Permanently
Location: https://yourapp.com/old-page
HTTP/1.1 301 Moved Permanently
Location: https://yourapp.com/old-page
...
Chrome and Firefox show a more direct version of the same problem:
ERR_TOO_MANY_REDIRECTS
This is especially common right after adding a rule for forcing HTTPS or trailing slashes.
Why It Happens
Nginx re-evaluates rewrite and location matching on every request, including the rewritten URL of a redirect if the client follows it right back to the same server. A loop happens when the destination of a rewrite still satisfies the same condition that triggered it. Common patterns:
- An HTTPS-forcing rule that doesn't correctly detect HTTPS β checking
$scheme != 'https'behind a reverse proxy or load balancer that terminates TLS upstream, where Nginx itself only ever sees plain HTTP traffic even for requests that were originally HTTPS, so every request appears to need the redirect, endlessly. - A trailing-slash rule that doesn't anchor its match correctly, redirecting a URL to a version of itself that still matches the same pattern.
- Two separate rewrite rules that redirect to each other β one forces a trailing slash, another strips it, creating a two-step cycle rather than an immediate self-redirect.
- A rewrite rule using
rewrite ... last;that re-enters the samelocationblock with a URL that still matches the original regex, causing Nginx to reprocess it in a cycle.
The Fix
For the HTTPS-behind-a-proxy case specifically, don't check $scheme directly if TLS is terminated upstream β check the forwarded protocol header instead, which the proxy should be setting:
# Wrong when behind a TLS-terminating load balancer
server {
listen 80;
if ($scheme != "https") {
return 301 https://$host$request_uri;
}
}
# Correct: check what the upstream proxy tells you
server {
listen 80;
if ($http_x_forwarded_proto != "https") {
return 301 https://$host$request_uri;
}
}
For a trailing-slash rule looping on itself, make sure the match explicitly excludes URLs that already satisfy the target condition:
# Wrong: matches its own output
rewrite ^/(.*[^/])$ /$1/ permanent;
# Better: use a dedicated directive instead of a raw rewrite for this
location /old-page {
return 301 /old-page/;
}
If two separate rules are redirecting to each other, trace the chain manually with verbose curl output to see the exact sequence of redirects and identify which two rules are conflicting:
curl -IL https://yourapp.com/old-page
HTTP/1.1 301 Moved Permanently
Location: /old-page/
HTTP/1.1 301 Moved Permanently
Location: /old-page
This confirms two distinct rules are fighting over the trailing slash β review both and keep only one direction of the redirect, removing whichever rule creates the reverse.
Still Not Working?
If the loop persists after fixing the obvious rule, check for rewrite directives inherited from an included config file or a parent server block that you don't immediately see in the file you're editing β dump the fully resolved configuration to see everything actually in effect for the request:
nginx -T | grep -B2 -A2 "rewrite\|return 301\|return 302"
This surfaces every redirect-related directive across all included files at once, which is often the fastest way to spot a forgotten rule from an old configuration template that's silently conflicting with a newer one you added later.
It's also worth understanding the difference between rewrite ... last; and rewrite ... permanent; (or redirect;), since mixing them up is a frequent source of these loops. A last flag tells Nginx to stop processing the current set of rewrite rules and re-search for a matching location block internally, entirely within the same request β no redirect is sent to the browser at all. A permanent or redirect flag, by contrast, sends an actual HTTP 301 or 302 response back to the client, who then issues a brand-new request. If a rule using last re-enters a location that itself contains the same rewrite, you get an internal loop that can max out Nginx's internal rewrite cycle limit rather than a browser-visible redirect loop, producing a different but related error:
2026/08/07 [error] rewrite or internal redirection cycle while processing "/old-page"
If you see this internal-cycle wording instead of a browser-side redirect loop, the fix is the same in spirit β break the self-matching condition β but the debugging starts inside Nginx's own rewrite log rather than in curl's redirect chain, since the browser never actually receives more than the final response or error in this internal-loop scenario.