The Nginx rewrite and redirect cycle error happens when Nginx gets stuck in an infinite loop, repeatedly matching a rewrite rule and then redirecting to a URL that itself matches the same rewrite rule.

Common Causes and Fixes

1. Trailing Slash Mismatch:

  • Diagnosis: Check your rewrite rules and location blocks. A common culprit is a rewrite rule that adds or removes a trailing slash, but the location block or another rewrite rule immediately behind it also tries to manipulate the trailing slash, leading to oscillation. For example, a rule might redirect /foo to /foo/, but another rule then sees /foo/ and redirects it back to /foo.
  • Fix: Ensure your rewrite rules and location directives consistently handle trailing slashes. If a rewrite rule is intended to add a trailing slash, make sure subsequent rules or the location block expect it.
    # Example of a problematic rule that might cause a cycle
    # rewrite ^/old-path$ /new-path/ permanent; # Adds slash
    
    # Another rule or location block that might then remove it or re-process
    # location /new-path/ { ... }
    # or
    # rewrite ^/new-path$ /new-path/ permanent; # Tries to add slash again
    
    # Corrected approach:
    location = /old-path {
        rewrite ^ /new-path/ permanent; # Redirects to /new-path/
    }
    location /new-path/ {
        # This block correctly handles the trailing slash added by the rewrite.
        # If you need to enforce a trailing slash, ensure it's handled here.
        # For example, if you *don't* want trailing slashes on /new-path,
        # you'd need a different rewrite rule or a location block that handles that.
        # But for preventing cycles, consistency is key.
        # If the rewrite adds a slash, the location should expect it.
        try_files $uri $uri/ =404;
    }
    
  • Why it works: By ensuring that a rewrite adding a slash is followed by a configuration that correctly handles that slash (or vice-versa), you break the cycle of adding/removing the slash repeatedly.

2. Case Sensitivity Mismatch:

  • Diagnosis: Nginx’s rewrite and location directives can be case-sensitive or case-insensitive depending on configuration. If a rewrite rule matches a URL in a case-insensitive way (e.g., rewrite ... "i";), but the subsequent location block or another rewrite rule is case-sensitive and doesn’t match the rewritten URL’s case, it might trigger another rewrite or redirect.
  • Fix: Use the if_mod flag with your rewrite rules or ensure your location blocks are consistently case-sensitive or insensitive as intended. Often, explicitly matching case is safer.
    # Problematic: Might rewrite /Page to /page, then a case-sensitive location /page/ doesn't match
    # rewrite ^/Page$ /page/ permanent;
    # location /page/ { ... }
    
    # Corrected: Ensure case consistency or use case-insensitive matching appropriately
    location /page/ {
        # This location block will match /page/ and /PAGE/ if the server is configured for case-insensitivity
        # or if the location itself is case-insensitive.
        # If you need explicit case handling in rewrites:
        rewrite ^/Page$ /page/ permanent; # Explicitly matches /Page
        rewrite ^/page$ /page/ permanent; # Explicitly matches /page
        try_files $uri $uri/ =404;
    }
    
  • Why it works: Explicitly defining case handling in rewrite rules and ensuring location blocks match the intended case prevents Nginx from misinterpreting the URL’s casing and re-evaluating rules incorrectly.

3. Incomplete or Overlapping rewrite Rules:

  • Diagnosis: You might have multiple rewrite rules that can match the same incoming URL, or a rewrite rule that doesn’t fully resolve the URL, leaving it in a state that matches another rule. This is particularly common with complex regular expressions.
  • Fix: Review your rewrite rules from top to bottom. Ensure that each rewrite rule has a clear termination condition (e.g., last or break if it’s meant to stop processing within the current location, or permanent/redirect if it’s a client-side redirect). Use last to stop processing rewrite directives in the current location and start a new search for a location matching the rewritten URI. Use break to stop processing rewrite directives in the current location and continue processing in the same location.
    # Problematic: Two rules could potentially match and restart the cycle
    # rewrite ^/old/(.*)$ /new/$1 permanent;
    # rewrite ^/old-path$ /new-path/ permanent; # If /old-path is also matched by the first rule, ambiguity
    
    # Corrected: Use 'last' to break the rewrite chain and re-evaluate locations
    location /old/ {
        rewrite ^/old/(.*)$ /new/$1 last; # Rewrites and searches for a new location
    }
    location /new/ {
        # This block handles URIs starting with /new/
        try_files $uri $uri/ =404;
    }
    
    # If a specific path needs a different rewrite, place it before or after based on priority
    location / {
        rewrite ^/specific-old-path$ /specific-new-path/ permanent; # Permanent redirect
        rewrite ^/another-old/(.*)$ /another-new/$1 last;       # Internal rewrite
        try_files $uri $uri/ =404;
    }
    
  • Why it works: The last directive tells Nginx to stop processing rewrite rules in the current location and restart the search for a location that matches the new URI. This prevents a cascade of rewrite directives within the same location block, which is a common source of cycles.

4. Missing or Incorrect location Block for Rewritten URI:

  • Diagnosis: A rewrite rule successfully changes the URI, but there’s no corresponding location block to handle the new URI. Nginx, unable to find a location, might re-evaluate the original rewrite rules, leading to a cycle.
  • Fix: Ensure that for every rewrite rule that changes a URI, there is a location block defined that will match the resulting URI.
    # Problematic: rewrite changes URI to /target, but no location /target/ exists
    # rewrite ^/source/(.*)$ /target/$1 permanent;
    
    # Corrected: Add a location for the rewritten URI
    location /source/ {
        rewrite ^/source/(.*)$ /target/$1 permanent;
    }
    location /target/ {
        # This block will now handle requests that were rewritten from /source/
        try_files $uri $uri/ =404;
    }
    
  • Why it works: By providing a location block that correctly handles the URI after a rewrite, Nginx can process the request without needing to re-evaluate the rewrite rules that led to it, breaking the potential cycle.

5. Using rewrite Inside location ~* or location ~ Without last or break:

  • Diagnosis: Regular expression location blocks (~ for case-sensitive, ~* for case-insensitive) can be tricky. If a rewrite rule within such a block doesn’t use last or break, Nginx might continue processing other rewrite directives in the same location block or even re-evaluate the original location block with the modified URI, leading to a loop.
  • Fix: Always use last or break when using rewrite inside a regular expression location block to control how Nginx proceeds after the rewrite. last is generally preferred as it restarts the location search.
    # Problematic: rewrite inside ~* without last/break might loop
    # location ~* \.php$ {
    #     rewrite ^/app/(.*)$ /index.php?q=$1; # No last/break, might re-evaluate itself or other rules
    #     fastcgi_pass unix:/var/run/php-fpm.sock;
    #     ...
    # }
    
    # Corrected: Use 'last' to break the rewrite chain and re-evaluate locations
    location ~* \.php$ {
        rewrite ^/app/(.*)$ /index.php?q=$1 last; # Use last to ensure Nginx searches for a new location for /index.php
        # If /index.php has its own location block, Nginx will find it.
        # If not, and you want this location to handle it:
        # try_files $uri =404; # Or use a specific fastcgi_pass for index.php
        # fastcgi_pass unix:/var/run/php-fpm.sock;
        # ...
    }
    
    # Example of a location for index.php itself
    location /index.php {
        fastcgi_pass unix:/var/run/php-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
    
  • Why it works: last ensures that the rewrite directive terminates its processing within the current location and that Nginx begins a new search for a matching location based on the rewritten URI. This prevents Nginx from re-evaluating the same location block with the modified URI indefinitely.

6. Incorrectly Using return with a URI that Matches a rewrite Rule:

  • Diagnosis: The return directive is intended to stop processing and return a specific status code or URL. If you use return to redirect to a URL that itself matches an active rewrite rule, you’ll create a cycle.
  • Fix: Ensure that any URL specified in a return directive does not trigger any rewrite rules that would then redirect it back to the original URL or a URL that would cause another return to be evaluated in a loop.
    # Problematic: return redirects to /old-page, which is then rewritten back
    # rewrite ^/old-page$ /new-page permanent;
    # location / {
    #    return 301 /old-page; # This will cause a cycle if /old-page is matched by the rewrite above
    # }
    
    # Corrected: Return a URL that doesn't trigger the rewrite, or perform the rewrite directly.
    location / {
        # Option 1: Perform the rewrite directly
        rewrite ^/old-page$ /new-page permanent;
        # Option 2: If you *must* use return, ensure the target doesn't trigger a loop
        # For example, if you wanted to redirect to a *different* page that doesn't have a rewrite rule
        # return 301 /another-page-without-rewrites;
    }
    
  • Why it works: The return directive halts processing immediately. If its target URL is designed not to re-engage the problematic rewrite rule, the cycle is broken. Typically, it’s better to use rewrite for internal redirects and return for external or final redirects.

The next error you’ll likely encounter after fixing rewrite cycles is a 404 Not Found error if your rewrites lead to URIs that no location block can satisfy.

Want structured learning?

Take the full Nginx course →