How Nginx location Block Matching Actually Works
Introduction¶
"Why is /api/ hitting the wrong location?" comes up constantly. Nginx's
matching rules are precise but not obvious — prefix and regex locations follow
different rules and interact in a specific order. This is the whole algorithm.
The location types¶
| Syntax | Type | Notes |
|---|---|---|
location /path/ |
Prefix | Matches URIs starting with /path/ |
location = /exact |
Exact | Matches only that exact URI. Checked first, stops immediately. |
location ^~ /path/ |
Prefix, non-regex | If it's the longest prefix match, skip regex checks |
location ~ \.php$ |
Regex, case-sensitive | |
location ~* \.(jpg\|png)$ |
Regex, case-insensitive | |
location @name |
Named | Only reachable via try_files / error_page, never from a request directly |
The matching order¶
For an incoming URI, Nginx does this, in order:
- Exact match (
=) — if one matches, use it and stop. Done. - Prefix matches — find the longest matching prefix location. Remember it.
- If that longest prefix has
^~, use it and stop — regex is skipped. - Otherwise, check regex locations in the order they appear in the file. The first regex that matches wins and stops the search.
- If no regex matched, fall back to the longest prefix from step 2.
Two things people get wrong:
- Regex order is file order, not longest-match. The first
~block that matches wins, even if a later one is "more specific". - A matching regex beats a longer prefix (unless the prefix has
^~or=). Solocation /images/loses tolocation ~ \.png$for/images/logo.png.
Examples¶
Serve static assets without hitting PHP¶
# ^~ means: if this is the longest prefix, don't even check the regex blocks
location ^~ /static/ {
root /var/www;
expires 30d;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php-fpm.sock;
include fastcgi_params;
}
Without ^~, a request for /static/config.php would match location /static/
as the longest prefix, then still be handed to the PHP block by the regex.
Exact match for a health check (fastest possible)¶
location = /healthz {
access_log off;
return 200 "ok\n";
}
= is checked first and stops immediately — no other block is evaluated.
Proxy an API prefix¶
location /api/ {
proxy_pass http://backend; # trailing slash behaviour matters, see below
}
The proxy_pass trailing-slash trap¶
location /api/ {
proxy_pass http://backend/; # WITH slash: /api/users -> backend/users
}
location /api/ {
proxy_pass http://backend; # WITHOUT slash: /api/users -> backend/api/users
}
With a URI after the host in proxy_pass, Nginx replaces the matched
location prefix. Without one, it passes the full original URI. This flips a
lot of "my routes are doubled / missing the prefix" bugs.
Verification and troubleshooting¶
nginx -T | grep -n "location" # every effective location, in order
# Add a header to see which block handled a request
# add_header X-Debug-Location "static" always;
curl -sI https://example.com/static/x.png | grep X-Debug-Location
- Regex block never hit — an earlier regex in the file matches first, or a
^~/=prefix is short-circuiting. Reorder, or make the prefix non-^~. - Static file returns the PHP handler — add
^~to the staticlocation, or put alocation ~ \.php$ { ... }guard inside it. try_filesgives 404 for a file that exists — paths intry_filesare relative toroot/alias; a mismatchedalias(which replaces, not appends) is the usual cause.aliasneeds a trailing slash to match alocationwith one.- Everything hits the
/location — your specific locations have a typo or the wrong modifier;nginx -Tand read them top to bottom as Nginx would. - Config test passes, behaviour unchanged — you edited a file not
included, or there's aserver_namemismatch sending the request to a differentserver {}block.curl -H "Host: ..."and checknginx -Tfor the server that actually matches.
Related tools and reading¶
- On-site: HTTP Checker, Regex Tester, Security Headers Checker.
- Related posts: An Nginx reverse proxy with TLS, Rate limiting in Nginx.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Web & Mail Servers
Running BIND 9 as an Authoritative Server
BIND as an authoritative server: zone files, secured AXFR with TSIG, SOA serial rules, and DNSSEC inline-signing. Here's a working master/slave setup.
October 24, 2026Varnish Cache in Front of a Site
Varnish is a caching HTTP accelerator configured in VCL. Here's the request lifecycle, why you strip cookies, how bans work, and grace mode for serving stale during backend trouble.
October 24, 2026Let's Encrypt with Certbot, Including Renewal That Doesn't Break
Getting the first cert is easy; keeping renewal working for years is the part people get wrong. Webroot vs DNS-01, deploy hooks, and the failure modes.
October 24, 2026