Nginx location block priority explains why a request such as /assets/app.css may be handled by a regex block even when a normal location /assets/ block exists. The matching rules are not simple top-to-bottom order. Nginx first chooses the best literal prefix, then may still let the first matching regex override that prefix unless the selected prefix uses ^~.
The risk shows up most often after a small routing change: static assets start hitting an extension regex, an API path falls into the wrong proxy, or a fallback location catches requests that should have stayed in a narrower block. A local test server makes those outcomes visible before the same rule reaches a production virtual host.
Test location changes before applying them to a production virtual host. A broad regex, misplaced
^~prefix, or wrong fallback location can change routing for every request under that server block.
Understand Nginx Location Block Priority
The Nginx location directive selects configuration by matching the request URI. Nginx compares the normalized URI path, not the query string. A request for /app.js?v=2 is matched as /app.js, so query arguments do not make one location block win over another.
Nginx location priority follows this sequence:
- Check exact locations such as
location = /path. If one matches, Nginx stops searching. - Find the longest matching prefix location, including normal prefixes and
^~prefixes. - If the longest matching prefix uses
^~, Nginx uses that location and skips regex checks. - If the longest matching prefix does not use
^~, Nginx checks regex locations in the order they appear. - If the first matching regex exists, Nginx uses that regex location.
- If no regex matches, Nginx uses the longest prefix location found earlier.
That middle step is the part many configurations get wrong. A normal prefix block is remembered, but it does not automatically win. A regex block can still override it. A ^~ prefix wins only when it is the longest matching prefix for that URI.
Compare Nginx Location Block Modifiers
The modifier before the URI controls how the location is matched. Match the modifier to the path you need to protect, then decide whether regex blocks should still be allowed to run.
| Location Type | Example | How Nginx Treats It | Best Use |
|---|---|---|---|
| Exact match | location = /health | Matches only the exact URI and stops all further location searching. | Health checks, root path, favicon, one specific endpoint. |
| Priority prefix | location ^~ /assets/ | Wins over regex only when it is the longest matching prefix. | Static directories, uploads, protected paths that should not fall into a regex handler. |
| Normal prefix | location /api/ | Can be overridden by the first matching regex unless no regex matches. | General path routing, default site handling, application fallback paths. |
| Case-sensitive regex | location ~ \.php$ | Checked in file order after prefix selection unless the chosen prefix uses ^~. | PHP handlers, extension-specific routes, strict pattern matches. |
| Case-insensitive regex | location ~* \.(jpg|png)$ | Same priority as ~; the first matching regex in the config wins. | Static extension rules where uppercase and lowercase extensions should both match. |
| Named location | location @app | Not used for normal browser requests. It is reached by internal redirects such as try_files or error_page. | Application fallback, upstream fallback, internal error handling. |
Regex order matters because Nginx does not pick the most specific regex. It picks the first regex that matches after the prefix phase allows regex evaluation. Keep specific regex blocks above broad regex blocks, or use a ^~ prefix when an entire path should bypass regex handling.
Test Nginx Location Block Priority Safely
The easiest way to understand location priority is to run a local-only test server. This fixture listens on 127.0.0.1:8080, so it is not exposed publicly. If another service already uses port 8080, choose another unused local port and use the same port in the later curl commands.
First, confirm that your Nginx configuration loads a directory where a small test server file can live:
sudo nginx -T 2>/dev/null | grep -E 'include .*(conf\.d|sites-enabled)'
Relevant output often includes one or both of these include paths:
include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*;
Most packaged Nginx builds load /etc/nginx/conf.d/*.conf. If your source-built or custom configuration does not, place the test server block in a file that your nginx.conf already includes instead of blindly using the path below.
Create a temporary test file:
sudo nano /etc/nginx/conf.d/location-priority-test.conf
Paste this local-only server block:
server {
listen 127.0.0.1:8080;
server_name location-test.example;
default_type text/plain;
location = / {
return 200 "exact root location\n";
}
location ^~ /assets/private/ {
return 200 "priority prefix private assets\n";
}
location /assets/ {
return 200 "normal assets prefix location\n";
}
location ~* \.(css|js|png|jpg|jpeg|gif|webp)$ {
return 200 "regex static extension location\n";
}
location / {
return 200 "catch-all prefix location\n";
}
}
Test the Nginx configuration before reloading:
sudo nginx -t
A successful syntax test prints these lines:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
Reload Nginx after the syntax test passes:
sudo systemctl reload nginx
The reload command normally returns no output on success. If your system does not use systemd, reload Nginx with the service manager used by that host.
Confirm that the temporary server is listening on the loopback port before sending requests:
sudo ss -ltnp | grep -F '127.0.0.1:8080'
The matching line should show Nginx bound to 127.0.0.1:8080. If it does not appear, recheck the syntax output and the include path you used for the test file.
Read Nginx Location Block Priority Results
Run the test requests from the Nginx host. The Host header selects the test server block without requiring DNS changes:
curl -s -H "Host: location-test.example" http://127.0.0.1:8080/
curl -s -H "Host: location-test.example" http://127.0.0.1:8080/assets/app.css
curl -s -H "Host: location-test.example" http://127.0.0.1:8080/assets/private/app.css
curl -s -H "Host: location-test.example" http://127.0.0.1:8080/assets/readme
curl -s -H "Host: location-test.example" http://127.0.0.1:8080/about
The output should appear in this order:
exact root location regex static extension location priority prefix private assets normal assets prefix location catch-all prefix location
The second line is the important lesson. The request for /assets/app.css first matches the normal prefix /assets/, but that prefix does not use ^~. Nginx then checks regex locations, finds the static extension regex, and uses the regex block instead.
The third line shows the opposite result. The request for /assets/private/app.css matches the longer prefix ^~ /assets/private/, so Nginx skips the static extension regex and uses the priority prefix block.
Apply Nginx Location Block Priority in Real Sites
Real Nginx configurations usually mix static files, application routes, reverse proxies, and extension handlers. Start with path ownership first, then decide whether each path should be allowed to fall through to regex locations.
Use Exact Nginx Locations for One URI
Use exact locations for endpoints that should not be affected by broader prefix or regex blocks:
location = /health {
access_log off;
return 200 "ok\n";
}
location = /favicon.ico {
log_not_found off;
access_log off;
root /var/www/example/static;
}
An exact location for /health does not match /health/ or /health/check. Add a separate prefix or redirect if those paths should behave the same way.
Use Nginx Priority Prefix Locations for Directories That Must Bypass Regex
Use ^~ when a directory should stay in that prefix block even if a later regex could match the URI. Upload directories are a common example because uploaded files should usually be served as files, not executed by a PHP or application handler.
location ^~ /uploads/ {
root /var/www/example/public;
try_files $uri =404;
}
This block prevents a request such as /uploads/avatar.php from reaching a later location ~ \.php$ block, as long as ^~ /uploads/ is the longest matching prefix for that URI.
Order Nginx Regex Locations from Specific to Broad
Regex locations with ~ and ~* are checked in configuration order. Put narrow patterns before broad patterns when both can match the same request.
location ~ ^/api/v1/reports/.*\.json$ {
proxy_pass http://127.0.0.1:3001;
}
location ~ ^/api/ {
proxy_pass http://127.0.0.1:3000;
}
If the broad ^/api/ regex appears first, it catches /api/v1/reports/file.json before the more specific reports regex has a chance to run. Regex blocks are not sorted by detail or length.
Use Named Nginx Locations for Internal Fallbacks
A named location starts with @. It does not match a normal browser URL. Use it as a fallback target from directives such as try_files or error_page.
location / {
try_files $uri $uri/ @app;
}
location @app {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
The browser still requests /about or /dashboard. Nginx reaches @app internally only after try_files cannot find a matching static file or directory.
If this fallback proxies an application, the broader Nginx reverse proxy workflow covers proxy headers, upstreams, WebSocket handling, and HTTPS. If the fallback rewrites or redirects URLs instead, use the dedicated guides for Nginx rewrite rules and Nginx redirects.
Avoid Common Nginx Location Block Priority Mistakes
Most location bugs come from assuming one of three wrong rules: that Nginx checks every block from top to bottom, that a normal prefix always beats regex, or that the query string affects location selection.
| Mistake | What Actually Happens | Fix |
|---|---|---|
| Putting a broad regex above a specific regex | The first matching regex wins, even when a later regex is more specific. | Move the specific regex above the broad regex or replace the regex with a prefix route. |
Expecting location /assets/ to beat all image or CSS regex blocks | A normal prefix can be overridden by a later regex match. | Use location ^~ /assets/ only when the whole path should skip regex handling. |
Using ^~ on a parent path but expecting it to beat a longer child prefix | Nginx first finds the longest prefix. A shorter ^~ prefix does not automatically beat a longer normal prefix. | Put ^~ on the exact directory that should own the request path. |
Expecting /api and /api/ to behave the same in every location | They are different URIs. Slash-ending pass locations may redirect the slashless URI, while exact locations require separate blocks. | Add an exact location when the slashless URI needs a deliberate redirect or a separate response. |
Trying to browse to /@app | Named locations are internal and are not selected by normal request matching. | Reach named locations through try_files, error_page, or another internal redirect. |
| Changing a query string to test location priority | Nginx location matching uses the URI path, not query arguments. | Test with different paths, or handle query logic in the application or a separate map-driven rule. |
Trailing slashes also matter. location /api/ matches /api/users, but /api is a different URI. When a slash-ending prefix proxies or passes requests upstream, Nginx can automatically send a permanent redirect from the slashless URI to the slash-added URI. Add an exact location when you want to make that behavior explicit or handle /api differently:
If this route is a reverse proxy, the Nginx proxy_pass trailing slash guide covers what happens to the upstream URI after the location block is selected.
location = /api {
return 301 /api/;
}
location /api/ {
proxy_pass http://127.0.0.1:3000;
}
This makes the boundary explicit and keeps the application route under the prefix that actually owns /api/.
Troubleshoot Nginx Location Block Priority Problems
When a request reaches the wrong handler, prove three things in order: the request is reaching the expected server block, the expected location is eligible to match, and no regex or fallback is taking over later.
Inspect Active Nginx Location Blocks
Use the full Nginx configuration dump to inspect the merged runtime configuration. This catches included files that are easy to miss when you only open one site file:
sudo nginx -T 2>/dev/null | grep -E 'server_name|listen|location|root|alias|try_files|proxy_pass|fastcgi_pass|return'
Relevant lines may look like this:
listen 80;
server_name example.com www.example.com;
root /var/www/example/public;
location / {
try_files $uri $uri/ @app;
location ~ \.php$ {
fastcgi_pass unix:/run/php/php-fpm.sock;
location @app {
proxy_pass http://127.0.0.1:3000;
If the request is reaching the wrong server block, fix listen, server_name, or the default server first. Location priority only applies after Nginx has already selected the virtual server for the request.
Test the Nginx Location With a Host Header
Use the curl command with an explicit Host header to test the selected site from the Nginx server:
curl -I -H "Host: example.com" http://127.0.0.1/assets/app.css
Relevant headers include the status code and any headers added by the winning location:
HTTP/1.1 200 OK Content-Type: text/css
If the output shows a redirect, proxy response, PHP error, or unexpected content type, compare the URI against each prefix and regex block. A header added only inside one location is often the fastest way to prove which block won.
Fix Static Files Handled by the Wrong Nginx Location
Static files often go wrong when a normal prefix is supposed to own a directory, but a regex extension handler overrides it. For example, this normal prefix does not stop the later regex:
location /assets/ {
root /var/www/example/public;
}
location ~* \.(css|js|png|jpg|jpeg|gif|webp)$ {
root /var/www/example/public;
expires 30d;
}
If the whole /assets/ tree should stay in the prefix block, make the longest intended prefix a ^~ prefix:
location ^~ /assets/ {
root /var/www/example/public;
try_files $uri =404;
}
Do not add ^~ mechanically to every directory. If you rely on extension regex blocks to set cache headers or run a PHP handler, a ^~ prefix can intentionally block that regex behavior.
Fix a Correct Nginx Location With the Wrong File Path
Sometimes the right location wins, but the file lookup is still wrong. That is usually a root, alias, or try_files problem rather than a location priority problem.
location /downloads/ {
root /var/www/example;
}
location /media/ {
alias /mnt/media/;
}
With root, Nginx appends the URI to the root path. With alias, Nginx replaces the matching location prefix with the alias path. If the selected location is right but files still return 404, check the final filesystem path before changing priority modifiers.
For broader missing-file or missing-route troubleshooting, use the Nginx 404 Not Found guide after you confirm the winning location is the one you intended.
Clean Up the Nginx Location Block Priority Test
Remove the temporary local-only test server when you finish the examples:
sudo rm -f /etc/nginx/conf.d/location-priority-test.conf
Test and reload Nginx again:
sudo nginx -t && sudo systemctl reload nginx
A successful cleanup leaves the main configuration valid:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
If you placed the test server in a different included site file, remove that file or delete only the temporary server block you added. Do not remove a live production server block as part of the cleanup.
Conclusion
Nginx location routing is easier to debug when exact matches, longest prefixes, priority prefixes, regex order, and named fallbacks each have a clear job. Keep high-risk routing changes behind nginx -t, a controlled reload, and local curl probes before changing a live server block.


Formatting tips for your comment
You can use basic HTML to format your comment. Useful tags currently allowed in published comments:
<code>command</code>command<strong>bold</strong><em>italic</em><blockquote>quote</blockquote>