Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for NGINX By James Joyner IV · · 9 min read

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_files fallback to a missing file. The classic case is try_files $uri $uri/ /index.php; (or /index.html) where index.php does not exist on disk, or root points at the wrong directory. The fallback /index.php re-enters location matching, hits the same try_files, fails again, and loops.
  • error_page pointing at a path that also 404s. error_page 404 /index.php; will internally redirect 404s to /index.php. If /index.php itself cannot be served (wrong root, missing file, no PHP handler), it produces another 404, which re-triggers the same error_page, looping.
  • index directive naming a file that 404s. With index index.php; a request for / resolves to /index.php. If that file is missing, the 404 fallback re-enters try_files and cycles.
  • rewrite ... last that re-matches the same location. A rule like rewrite ^/(.*)$ /$1 last; (or any rewrite whose result still matches the location containing it) re-enters the same block and rewrites again.
  • Mismatched root between server and location. A location overrides root so 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

  1. 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.

  2. 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 a try_files fallback or an error_page.

  3. Verify the target can actually be served. Check root for that location, then ls -l the resolved file. If the file is missing, you have two choices: fix the path so it exists, or stop redirecting to it.

  4. Fix try_files so the final fallback is terminal. The final argument of try_files should 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.php must hit a location that handles it (FastCGI, proxy, real file) rather than re-entering try_files.

  5. Fix any looping error_page. Point error_page at 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. Avoid error_page 404 /index.php; unless /index.php resolves cleanly.

    error_page 404 /404.html;
    location = /404.html {
        internal;
        root /var/www/app/public;   # 404.html must exist here
    }
  6. Correct root mismatches. Make sure the root in the active location (or inherited from server) actually contains the index/fallback file. A location block can silently override root.

  7. Tame self-matching rewrite rules. If a rewrite ... last produces a URI that still matches the same location, scope the regex so the result no longer re-matches, or move the rule to a more specific location.

  8. Test and reload. Once the fallback resolves to something terminal, validate and reload:

    sudo nginx -t && sudo systemctl reload nginx
  9. Confirm the fix. Re-run curl -sI https://app.example.com/missing. You should now see 404 (honest not-found) or 200 (handler served it), and the error log should stop logging the cycle.

Prevention and Best Practices

  • Always terminate try_files with a non-recursive final argument: a file that exists, a named location, or =404.
  • Never redirect error_page to a path that can itself produce the same error code.
  • Keep error_page targets internal; and backed by static files that are guaranteed to exist.
  • After any root, try_files, or index change, run sudo nginx -T and confirm the resolved paths with ls -l before 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 root per server block and avoid re-declaring it inside location unless intentional.
  • 404 Not Found — what you usually want instead of the loop; ending try_files with =404 converts the cycle into an honest 404.
  • 502 Bad Gateway / upstream prematurely closed — appears when /index.php routes 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 — related index/autoindex resolution 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.

Free download · 368-page PDF

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.