2.3 The Location-Matching Algorithm
The most quizzed, most misquoted mechanism in NGINX. After this lesson you will never guess again — you will trace.
Basic
What you will learn
What a
locationis (and the three jobs it can hold).The five modifiers and their exact meanings.
The algorithm — memorized as four moves — with the priority pitfalls.
The decoded-URI rule (and the security consequence of
~vs~*).Nested locations,
@namedlocations, and when to use each.
1. What a location is
Inside a server, a
location answers
"for URIs shaped like this, do something different than the default." That "something" is exactly one of:
Serve static content differently — different
root/alias, different headers, differenttry_files(Modules 3–4).Proxy differently — different
proxy_pass, buffers, timeouts.Short-circuit entirely —
return/rewritebefore any content handling (this lesson's toys, Module 11's profession).
The alpha vhost in our example config is a deliberate
algorithm playground — fifteen locations, each announcing itself with an
X-Match header. Here is the live capture, loop included:
$ for u in /exact /docs/handbook.pdf /docs/payload.bin /images/hero.png \
/logo.png /logo.PNG /secret.txt /secret.TXT /about.html; do
printf '%-20s %-4s %s\n' "$u" "$(curl -s -o /dev/null -w '%{http_code}' \
-H 'Host: alpha.example.test' http://127.0.0.1:8200$u)" \
"$(curl -sD - -o /dev/null -H 'Host: alpha.example.test' \
http://127.0.0.1:8200$u | awk 'tolower($1)=="x-match:"{sub(/\r/,""); print $2, $3}')"
done
/exact 200 exact (=)
/docs/handbook.pdf 200 ~ (regex,
/docs/payload.bin 200 prefix /docs/
/images/hero.png 200 ^~ /images/
/logo.png 200 ~* (regex,
/logo.PNG 200 ~* (regex,
/secret.txt 200 ~ (regex,
/secret.TXT 404 (fell to prefix / — file doesn't exist)
/about.html 200 (prefix / — no X-Match of its own)One table, five modifiers, every rule visible. Let's formalize it.
2. The five modifiers
location = /exact { … } # EXACT: this URI and nothing else
location /docs/ { … } # PREFIX: URI starts with /docs/ (longest wins)
location ^~ /images/ { … } # PREFIX+, "preferential": if this prefix is the
# longest, DON'T even try regexes
location ~ \.(txt|pdf)$ { … } # REGEX, case-SENSITIVE, first declared wins
location ~* \.(png|jpg)$ { … } # REGEX, case-INSENSITIVE, first declared winsWhat each is for, in one breath:
=— the fastest possible match (one hash lookup, no walking) and a promise that/exactand only/exactlands here. Use it for the high-QPS hot paths:location = /healthz,location = /.plain prefix — the workhorse. Directories, sections, "everything under /api/". Longest prefix beats shorter ones, regardless of order in the file.
^~— "prefix, and I mean it": when the longest matching prefix is^~, regexes are skipped. The escape hatch for regex interference.~/~*— pattern matching on the URI: extensions, IDs, version schemes. Case matters on Linux filesystems — hence two flavors.
3. The algorithm, as four moves
When a request enters the chosen server, NGINX:
MOVE 1 — find all prefixes that match the (decoded) URI.
Remember the LONGEST one. If it is ^~-marked, jump to MOVE 4 with it.
MOVE 2 — run the regexes IN CONFIG ORDER (top of file downward,
across all locations in that server).
FIRST match wins → jump to MOVE 4 with it.
MOVE 3 — no regex matched: the longest prefix from MOVE 1 wins.
MOVE 4 — enter the winning location. Done. Exactly one location ever runs.Trace the table below through these moves:
URI | Move 1 longest prefix | Move 2 regexes | Winner | Why |
|---|---|---|---|---|
|
| none apply |
| exact check actually happens before Move 1 |
|
|
|
regex
| regexes outrank plain prefixes |
|
| none match
|
prefix
| longest prefix stands |
|
|
|
| preferential prefix slams the door |
|
|
|
regex
| |
|
|
|
regex
| case-insensitive in action |
|
|
|
regex
| |
|
|
|
prefix
| then file lookup fails → 404 |
The
= modifier technically participates before Move 1 — an exact match is a hash hit and ends the game immediately. (You can see it as "Move 0".)
4. The decoded-URI rule (and why
~ is a security feature)
Moves 1–2 operate on the
decoded, normalized URI — the value of
$uri, not the raw bytes the client sent. Percent-escapes are decoded
before matching:
client sends: GET /images%2Fhero.png (an escaped slash!)
NGINX matches: /images/hero.png → location ^~ /images/The regex modifiers then interact with decoding in a way that matters for security:
~(case-sensitive) matches against the decoded URI verbatim.~*lowercases the URI before matching (that's what "insensitive" means mechanically).
The classic vulnerability this enables — "upload
shell.PHP because the admin regex was
~ \.php$ but the handler checked case-insensitively" — is a mismatch between
two layers. Inside NGINX itself the rule is simpler:
the location that matches is the location that handles, case and all. Keep case-sensitive
~ for everything executable (
.php,
.py,
.cgi), and you inherit a small but real safety property:
/script.PHP simply won't route to your PHP upstream unless you told it to.
5. Nested and named locations
Nesting — legal, occasionally genius, mostly a trap
location /admin/ {
# auth, headers, logging for the admin tree…
location ~ \.php$ { # regexes INSIDE only compete after the outer
# …php-specific handling prefix matched
}
}Inner locations are consulted only among themselves, after the outer one matched — a private mini-tournament. Two honest uses: (a) the outer block holds shared directives (auth, headers) and the inner ones only refine the handler; (b) you need
^~-style insulation inside a subtree. The trap: nesting multiplies paths through the config; readers can no longer apply the four moves in one pass. Depth ≤ 1, and only with a comment saying why.
@named locations — internal jump targets
location / {
try_files $uri $uri/ @app; # Lesson 3.2's deep end
}
location @app {
proxy_pass http://app-pool; # never matched by any external URI
}A
@name location is invisible to the matching algorithm. It exists to be
jumped to — by
try_files,
error_page,
rewrite … last (Module 11) — giving you a named fallback bundle of directives. Think "function for request handling". Lesson 3.2 puts the first one to work.
Try it yourself
The playground has an open invitation:
Reproduce the table in §1 — run the exact loop and diff against the lesson. Every row should match.
Break the priority on purpose. Add
location /images/extra/ { … }(plain prefix) below the^~ /images/block, then request/images/extra/hero.png— wait, does hero.png exist there? (No — use the X-Match header to see WHO matched, regardless of 404.) Now addlocation ~ /extra/ { … }above everything and re-test: which wins? Restore afterwards.Prove prefixes are character prefixes. Request
/docsfoowith the X-Match readout —/docs/would match/docs…if written without the slash; with the slash,/docsfoofalls to/. Verify, then try/docs/../secret.txt(NGINX normalizes..before matching — you get/secret.txtand its regex location).Health-check idiom. Add
location = /healthz { return 200 "ok\n"; }and hammer it:
curl -s -H 'Host: alpha.example.test' http://127.0.0.1:8200/healthzFull sweep when you're done experimenting: re-run the §1 loop and diff — every row must still match.
Q&A
Q1.
location /api/v1/ and
location ~ ^/api/ — which serves
/api/v1/users? Which serves
/api/v2/users?
Answer
/api/v1/users: the regex wins (regexes outrank plain prefixes — Move 2 before Move 3).
/api/v2/users: also the regex (it matches the whole
/api/ tree). The prefix only ever serves URIs the regexes decline — here, none. That's the surprise the table's
handbook.pdf row teaches.
Q2. How do you make
/api/ bypass all regex locations without deleting them?
Answer
location ^~ /api/ { … } — the preferential prefix. (Assuming no longer prefix outruns it; prefixes still compete by length first.)
Q3. A request for
/IMG/logo.PNG is served by your
~* \.(png)$ location even though the files live under
/images/. Name two distinct reasons.
Answer
(1)
~* is case-insensitive —
.PNG matches. (2) There is no case-insensitive
prefix match:
/IMG/ won't hit
location /images/ or
^~ /images/ (prefix matching is case-sensitive), so the URI fell through to the regex. Fix: normalize case at the edge (
rewrite in Module 11) or cover both spellings.
Q4. Why does
location = / exist in so many high-performance configs when
location / already matches everything?
Answer
= / answers the single hottest URI (the homepage) with one hash lookup — no prefix-walk, no regex scan — and lets
location / stay a general catch-all. It's the same hot-path thinking as CPU caches: optimize the one entry everyone hits.
Takeaways
Four moves: exact hash-hit → longest prefix (if
^~: stop) → first regex in file order → fall back to longest prefix.Prefixes compete by length, regexes by position,
=by hash — three different currencies; never confuse them.Matching happens on the decoded URI;
$request_uriis the untouched original — the distinction explains percent-escape surprises and powers Module 11's rewrite work.^~is the sanctioned way to shield a subtree from regexes; nested locations are the unsanctioned way to multiply confusion.@namedlocations are internal jump targets — the "functions" you'll call fromtry_filesanderror_pagefrom the next module onward.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.