The Problem
You've just finished deploying a Single Page Application (SPA) or a Laravel project. Everything seems perfect until you refresh the page and hit a 500 Internal Server Error. When you dive into your Nginx error logs, you find this specific headache:
rewrite or internal redirection cycle while processing "/index.html"
This happens because Nginx is stuck in a loop. It looks for a file, fails to find it, jumps to a fallback, and that fallback triggers the exact same search logic. Nginx has a hard limit of 10 internal redirects. Once it hits that 11th attempt, it kills the request to save your CPU from melting.
The Environment
- OS: Modern Linux distros (Ubuntu 22.04+, Debian, RHEL)
- Software: Nginx (any version)
- Common Scenarios: Client-side routing in React/Vue, index.php routing in PHP frameworks, or custom 404 handlers.
How the Loop Starts
Usually, the trouble starts with a try_files directive that's a bit too optimistic. Look at this common (but flawed) configuration for a Vite-based React app:
server {
listen 80;
root /var/www/my-app/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
This tells Nginx to check for the literal file, then the directory. If both fail, it serves index.html.
The trap is simple: If index.html is actually missing from the /dist folder (perhaps due to a failed build), Nginx doesn't just stop. It redirects to /index.html, which then triggers the location / block again. It searches, fails, and redirects to /index.html for a second time. This repeats until Nginx hits the 10-cycle limit and gives up.
Step-by-Step Debugging
1. Inspect the Error Log
Identify which path is failing by tailing your logs in real-time. Run this command while refreshing your browser:
tail -f /var/log/nginx/error.log
Pay close attention to the path in the error. If it says while processing "/index.php", Nginx is telling you that index.php is the missing link in your chain.
2. Verify File Paths and Permissions
Check if the fallback file actually exists where you think it does. Run a quick ls on your root path:
ls -la /var/www/my-app/dist/index.html
If the file is there, Nginx might still be blocked from reading it. Use namei to check the full permission chain. Every directory from / down to your file must be executable (+x) by the www-data user:
namei -om /var/www/my-app/dist/index.html
3. Use Named Locations for Safety
Named locations are a cleaner alternative. They separate your file-checking logic from the actual fallback execution, preventing accidental recursion. This is the industry-standard way to handle SPAs.
For SPAs (React/Vue/Vite):
location / {
try_files $uri $uri/ @fallback;
}
location @fallback {
rewrite ^ /index.html break;
}
For PHP (Laravel/WordPress):
location / {
try_files $uri $uri/ /index.php?$query_string;
}
In PHP setups, the loop often happens inside the .php block. To stop the cycle, add =404 to the try_files directive inside your PHP handler. This ensures that if index.php is gone, Nginx returns a clean 404 instead of looping.
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
The Final Solution: A Robust Config
If you prefer a simple try_files setup, you must ensure the final parameter doesn't trigger a new search. Here is a battle-tested configuration for a modern web app:
server {
listen 80;
root /var/www/html;
index index.html;
location / {
# Try the file, then directory.
# If both fail, send the request to index.html.
try_files $uri $uri/ /index.html;
}
# Explicitly handle index.html to break potential loops
location = /index.html {
# The 'internal' directive prevents external users
# from calling this location directly if the file is missing.
internal;
}
}
Verification
Before you walk away, always validate your Nginx syntax. One missing semicolon can take down your whole site.
sudo nginx -t
If you see syntax is ok, apply the changes:
sudo systemctl reload nginx
Finally, test a non-existent route with curl. If you see a 200 OK (the SPA fallback) or a 404 instead of a 500, you've successfully broken the loop:
curl -I http://localhost/this-route-does-not-exist
Lessons Learned
- Fallbacks are redirects: The last item in
try_filesis an internal redirect. If that file is missing, the search starts over. - Root typos kill: Double-check your
rootpath. If Nginx is looking in/var/www/htmlbut your files are in/var/www/dist, it will loop forever. - Named locations are safer: Use
@namesyntax for complex routing to keep your logic clean. - Permissions matter: Nginx cannot serve what it cannot read. Ensure
www-dataowns the files.

