How nginx matches a request: locations and rewrite phases
Exact, prefix and regex location precedence, the order of the rewrite and access phases, return versus rewrite, and a method for reading an unfamiliar config.
Location matching precedence
server {
listen 443 ssl;
server_name example.com;
root /var/www/site;
location = /health { # 1. exact match, wins immediately
return 200 "ok\n";
add_header Content-Type text/plain;
}
location ^~ /static/ { # 2. longest prefix that stops regex checking
expires 30d;
access_log off;
}
location ~* \.(png|jpg|webp)$ { # 3. first matching regex in file order
expires 7d;
}
location /api/ { # 4. longest plain prefix if no regex matched
proxy_pass http://127.0.0.1:8080;
}
location / { # 5. the catch-all
try_files $uri $uri/ /index.html;
}
}- Order of evaluation: exact match, then longest
^~prefix, then regexes in the order written, then the longest plain prefix. - A regex location is checked as soon as no exact or
^~match applies, so a broad regex can steal requests from a more specific prefix. =is a single string comparison and is the cheapest match available — ideal for health checks and hot paths.- Nested locations are matched against the URI, not against the parent's file system path.
The request phases in order
| Phase | What runs | Typical directive |
|---|---|---|
| post-read | After the request line | Real IP resolution |
| server-rewrite | Server-level rewriting | rewrite in server |
| find-config | Choose the location | n/a |
| rewrite | Location-level rewriting | rewrite, return |
| access | Authorisation | allow, deny, auth_basic, limit_req |
| content | Produce the response | proxy_pass, index, try_files |
| log | Write the access entry | log_format |
# return ends the request immediately, in the rewrite phase,
# before access controls run. It is cheap and cannot be bypassed by a file.
location = /old-page {
return 301 https://example.com/new-page;
}
# an internal rewrite re-runs location matching, which is why it costs more
location /legacy/ {
rewrite ^/legacy/(.*)$ /new/$1 last;
}⚠️
A
return in the rewrite phase happens before allow and deny. If you rely on an access rule to protect a path you also redirect, the redirect fires first and the protection never runs.Reading an unfamiliar config
# 1. what is the fully expanded config, in order?
nginx -T > /tmp/expanded.conf
# 2. which server handles this host?
grep -n "server_name" /tmp/expanded.conf
# 3. which location matches this URI? add a marker and reload
# location = /debug-match { return 200 "matched exact\n"; }
# 4. test with a real request and inspect the result
curl -sSI https://example.com/static/app.css | head -20
curl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' https://example.com/api/healthWork top down: the server block by server_name and listen port, then the location by the precedence rules, then the directives inside it, remembering that inherited values apply where the location does not override them.
FAQ
What does <code>^~</code> actually change?
It stops nginx from evaluating regex locations when that prefix matches. Use it for static asset directories so a broad asset regex cannot take precedence and apply the wrong headers.
Why does my regex location never match?
A longer prefix with
^~ matched first, or the regex is checked after an exact match. Confirm the evaluation order before debugging the pattern itself.Related
Rewrites, redirects and try_files Installing nginx and understanding the configuration layout
Last refreshed 2026-09-18.