NGINX Error Guide: 'rewrite or internal redirection cycle' Infinite Loop
Fix the NGINX 'rewrite or internal redirection cycle while internally redirecting' 500 caused by looping try_files, rewrite, error_page and index rules.
- #nginx
- #troubleshooting
- #errors
- #config
Exact Error Message
When NGINX gives up on an internal redirect loop, it writes a line like this to the error log and returns a 500 Internal Server Error to the client:
2026/06/27 12:03:51 [error] 2841#2841: *771 rewrite or internal redirection cycle while internally redirecting to "/index.php", client: 203.0.113.7, server: app.example.com, request: "GET /missing HTTP/1.1", host: "app.example.com"
The exact target inside the quotes varies depending on your configuration. You may see "/index.php", "/index.html", "/404.html", or any path that your try_files, index, error_page, or rewrite directives keep pointing back to.
What the Error Means
NGINX processes a request by repeatedly resolving it against location blocks. Directives such as try_files, rewrite ... last, index, and error_page can issue an internal redirect: NGINX takes the rewritten URI and starts the location-matching process again, without involving the client. The browser never sees these hops.
To protect itself from configuration loops, NGINX caps the number of internal redirects per request at 10. If a request is internally redirected ten times without ever reaching a terminal handler that produces a response (a real file, an upstream, a return, etc.), NGINX aborts, logs rewrite or internal redirection cycle while internally redirecting to "...", and sends a bare 500 to the client.
So the error does not mean NGINX crashed. It means your configuration describes a path that never resolves: every internal redirect lands somewhere that triggers another internal redirect, forever. The client sees a 500; the cause is in the error log, not the access log.
Common Causes
try_filesfallback to a missing file. The classic case istry_files $uri $uri/ /index.php;(or/index.html) whereindex.phpdoes not exist on disk, orrootpoints at the wrong directory. The fallback/index.phpre-enters location matching, hits the sametry_files, fails again, and loops.error_pagepointing at a path that also 404s.error_page 404 /index.php;will internally redirect 404s to/index.php. If/index.phpitself cannot be served (wrongroot, missing file, no PHP handler), it produces another 404, which re-triggers the sameerror_page, looping.indexdirective naming a file that 404s. Withindex index.php;a request for/resolves to/index.php. If that file is missing, the 404 fallback re-enterstry_filesand cycles.rewrite ... lastthat re-matches the same location. A rule likerewrite ^/(.*)$ /$1 last;(or any rewrite whose result still matches the location containing it) re-enters the same block and rewrites again.- Mismatched
rootbetweenserverandlocation. Alocationoverridesrootso files resolve under a directory that does not contain the fallback target, even though the file “exists” elsewhere.
How to Reproduce the Error
Create a minimal server block with a fallback that can never resolve, then request any missing URI:
server {
listen 80;
server_name app.example.com;
root /var/www/app/public; # does this directory actually contain index.php?
location / {
try_files $uri $uri/ /index.php; # /index.php missing -> loop
}
}
If /var/www/app/public/index.php does not exist (or root is wrong), then:
curl -sI http://app.example.com/missing
returns HTTP/1.1 500 Internal Server Error, and the error log shows the redirection-cycle message above.
Diagnostic Commands
Start by confirming the configuration is syntactically valid, then dump the effective configuration so you can see exactly which try_files, error_page, index, and rewrite rules apply, plus the resolved root:
# Validate syntax (catches typos, not logic loops)
sudo nginx -t
# Dump the full merged configuration and grep the looping directives
sudo nginx -T | grep -nE 'root|try_files|error_page|index|rewrite'
Identify the root for the relevant server/location, then verify on disk that the document root and the fallback file actually exist:
# Confirm the document root exists and is readable
ls -l /var/www/app/public/
# Confirm the specific fallback target really exists
ls -l /var/www/app/public/index.php
If ls cannot find index.php, you have found the loop: try_files keeps falling back to a file that is not there.
Reproduce the 500 against the live server and watch the logs:
# Trigger the cycle and confirm the 500 status
curl -sI https://app.example.com/missing
# Tail NGINX's own logs for the cycle message
sudo journalctl -u nginx --no-pager -n 50
# Or read the error log directly
sudo tail -n 50 /var/log/nginx/error.log
Step-by-Step Resolution
-
Read the error-log target. The string inside the quotes (
"/index.php","/404.html", etc.) is the URI NGINX kept redirecting to. That path is the center of the loop. -
Find the directive that produces it. Use
sudo nginx -T | grep -nE 'try_files|error_page|index|rewrite'and locate every rule whose result equals the looping target. Usually it is atry_filesfallback or anerror_page. -
Verify the target can actually be served. Check
rootfor thatlocation, thenls -lthe resolved file. If the file is missing, you have two choices: fix the path so it exists, or stop redirecting to it. -
Fix
try_filesso the final fallback is terminal. The final argument oftry_filesshould either be a file that exists, a named location backed by a real handler, or an explicit response code so it cannot loop:# If there is no PHP/app handler, end with =404 (no loop, honest 404) location / { try_files $uri $uri/ =404; } # If there IS a PHP app, route the fallback to a handler location, # not back through the same try_files location / { try_files $uri $uri/ /index.php?$query_string; } location = /index.php { fastcgi_pass unix:/run/php/php-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root/index.php; }The key is that
/index.phpmust hit a location that handles it (FastCGI, proxy, real file) rather than re-enteringtry_files. -
Fix any looping
error_page. Pointerror_pageat a static file that exists and is served terminally, using=to set the final status and=404 /404.html;style targets backed by a real file. Avoiderror_page 404 /index.php;unless/index.phpresolves cleanly.error_page 404 /404.html; location = /404.html { internal; root /var/www/app/public; # 404.html must exist here } -
Correct
rootmismatches. Make sure therootin the activelocation(or inherited fromserver) actually contains the index/fallback file. Alocationblock can silently overrideroot. -
Tame self-matching
rewriterules. If arewrite ... lastproduces a URI that still matches the samelocation, scope the regex so the result no longer re-matches, or move the rule to a more specific location. -
Test and reload. Once the fallback resolves to something terminal, validate and reload:
sudo nginx -t && sudo systemctl reload nginx -
Confirm the fix. Re-run
curl -sI https://app.example.com/missing. You should now see404(honest not-found) or200(handler served it), and the error log should stop logging the cycle.
Prevention and Best Practices
- Always terminate
try_fileswith a non-recursive final argument: a file that exists, a named location, or=404. - Never redirect
error_pageto a path that can itself produce the same error code. - Keep
error_pagetargetsinternal;and backed by static files that are guaranteed to exist. - After any
root,try_files, orindexchange, runsudo nginx -Tand confirm the resolved paths withls -lbefore reloading. - Test missing URIs (
curl -sI .../does-not-exist) in staging, not just happy-path requests, so loops surface before production. - Pin one source of truth for
rootper server block and avoid re-declaring it insidelocationunless intentional.
Related Errors
404 Not Found— what you usually want instead of the loop; endingtry_fileswith=404converts the cycle into an honest 404.502 Bad Gateway/upstream prematurely closed— appears when/index.phproutes to FastCGI/PHP-FPM but the socket or pool is misconfigured.open() ... failed (2: No such file or directory)— the disk-level symptom that often precedes a cycle: NGINX cannot find the fallback file.directory index of "..." is forbidden— relatedindex/autoindexresolution problem in the same family of directives.
More NGINX troubleshooting guides live under the NGINX category.
Frequently Asked Questions
Why does the client get a 500 and not a 404?
Because NGINX never reaches a handler that can produce a real status. It exhausts its internal redirect limit (10) first, so it aborts the request with a generic 500. The actual diagnostic (“rewrite or internal redirection cycle”) only appears in the error log, never to the client.
What exactly is the limit that triggers the message?
NGINX allows at most 10 internal redirects for a single request. Each try_files fallback, error_page redirect, index resolution, or rewrite ... last that re-enters location matching counts as one hop. The eleventh would-be redirect triggers the cycle error.
My index.php exists, so why am I still looping?
The file existing on disk is not enough; it must be servable from the active root and reach a real handler. A location block can override root to a directory that lacks the file, or there may be no FastCGI/proxy directive to execute it, so the fallback 404s and re-enters try_files. Confirm with sudo nginx -T plus ls -l "$root/index.php".
Is try_files $uri $uri/ /index.php always wrong?
No. It is correct when /index.php is handled by a dedicated location (FastCGI or proxy) that actually runs it. It loops only when /index.php is missing or unhandled and therefore falls back into the same try_files. The fix is making that final fallback terminal.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.