backend / protocols / nginx / 04_location_matching.md

Nginx Location Matching

6 interview angles 5 min read source

Nginx Location Matching

location blocks match request URIs. The matching rules are not “first one wins” — they have a specific priority that confuses everyone the first time. This is one of the top interview gotchas.

The five forms

Form Syntax Example
Exact match location = /path location = /favicon.ico
Prefix with stop location ^~ /path location ^~ /static/
Case-sensitive regex location ~ pattern location ~ \.php$
Case-insensitive regex location ~* pattern location ~* \.(jpg|png)$
Plain prefix location /path location /api/
Named location location @name location @fallback (used by try_files, error_page)

The matching algorithm — memorize this

Nginx evaluates locations in this order:

  1. Exact match (=) — if a location = matches, use it. Stop.
  2. All prefix matches — find the longest matching prefix.
    • If the longest match has ^~, use it. Stop. Skip regexes.
  3. Regex matches (~ and ~*) — try each in the order they appear in config. First match wins.
  4. If no regex matched — use the longest prefix from step 2.

So the priority is: =^~ longest prefix → regex (in file order) → plain prefix (longest).

Worked example

server {
    location = / {
        # Block A: exact match for "/"
    }
    location / {
        # Block B: matches everything (catch-all prefix)
    }
    location /api/ {
        # Block C: prefix
    }
    location ^~ /static/ {
        # Block D: prefix with regex-skip
    }
    location ~ \.php$ {
        # Block E: regex, .php files
    }
    location ~* \.(jpg|png|gif)$ {
        # Block F: regex, image extensions case-insensitive
    }
}
Request URI Matched block Why
/ A exact match (=) wins
/index.html B no =, no ^~, no regex matches; longest prefix
/api/users C longer prefix than /; no regex matches
/static/app.css D ^~ matches, skip regex search
/static/logo.png D ^~ matches; without ^~, regex F would have won
/script.php E longest prefix is /; regex matches
/photo.jpg F longest prefix is /; regex matches

The /static/logo.png case is the classic gotcha — without ^~, the file-extension regex would override your static-file location.

Prefix vs regex priority — the most common mistake

location /images/ {
    root /var/www/cdn;
}

location ~ \.(jpg|png)$ {
    root /var/www/wrong;
}

A request for /images/cat.jpg:

  • Longest prefix: /images/.
  • Regex \.(jpg|png)$ also matches.
  • Regex wins (no ^~). Served from /var/www/wrong.

Fix:

location ^~ /images/ {
    root /var/www/cdn;
}

Now the prefix wins; regex isn’t tried.

= for hot exact matches

location = is the fastest match (single hash lookup). Use for high-volume exact endpoints:

location = / {
    # homepage
}
location = /healthz {
    return 200 "ok\n";
    access_log off;
}
location = /favicon.ico {
    log_not_found off;
    access_log off;
}

These bypass the slower regex engine entirely.

Named locations

Named locations (@name) aren’t matched against URIs — they’re targets for internal redirects.

location / {
    try_files $uri $uri/ @app;
}

location @app {
    proxy_pass http://app_backend;
}

try_files checks the file paths in order; if none exist, it routes to @app.

Nested locations

location /api/ {
    proxy_pass http://api;

    location /api/admin/ {
        allow 10.0.0.0/8;
        deny all;
        proxy_pass http://api;
    }
}

Nested locations share the parent’s matched URI namespace. Used for “this subset of the path has different rules.”

root vs alias — quick reminder

(Comes up because they affect what URI maps to which file path):

location /static/ {
    root /var/www;          # serves /var/www/static/file.css
}

location /static/ {
    alias /var/www/files/;  # serves /var/www/files/file.css (no /static/ in path)
}

root appends the URI to the path. alias replaces the location prefix with the path. Trailing slash on alias matters; on root doesn’t.

Order of regex location blocks matters

Regex locations are tried in the order they appear:

location ~ \.(jpg|png)$ { ... }           # block 1
location ~ ^/old/.+\.jpg$ { ... }         # block 2 — never matched

Block 2 never wins because block 1 matches first. Reorder so the more-specific regex comes first.

Common interview confusions

  • “First location block in the file wins.” — false. Exact > ^~ longest prefix > regex in order > longest plain prefix.
  • “Regex always wins over prefix.” — only when the prefix is plain. ^~ makes the prefix beat regex.
  • location / matches only /.” — it’s a prefix, matches everything. Use location = / for the exact root.
  • “Case in regex is automatic.”~ is case-sensitive, ~* is case-insensitive. Different operators.

Quick test: what matches what?

location = /a   { return 200 "A\n"; }
location ^~ /b/ { return 200 "B\n"; }
location ~ \.py$ { return 200 "C\n"; }
location /     { return 200 "D\n"; }
location /b/c/ { return 200 "E\n"; }
URI Result
/a A (exact)
/b/anything.py B (^~ skips regex)
/b/c/x B (longest ^~ prefix; ^~ set on /b/, beats /b/c/’s plain prefix? No — ^~ requires being the longest prefix. /b/c/ is longer, so /b/c/ wins → E)

Wait, let me redo that one:

/b/c/x:

  • Longest prefix: /b/c/ (matches; longer than /b/).
  • That prefix doesn’t have ^~, so regex search runs. No regex matches /b/c/x.
  • Use longest plain prefix: /b/c/ → E.

For /b/c/x.py:

  • Longest prefix is still /b/c/.
  • That prefix has no ^~, so regex runs. \.py$ matches → C.

So the ^~ only protects /b/ itself; /b/c/ (longer) wins for paths under it. The lesson: ^~ doesn’t propagate to longer prefixes.

This is genuinely confusing. Read the matching rules carefully or run quick tests with return 200 "<label>\n"; blocks.

Interview angle

  • “In what order does nginx evaluate location blocks?” — exact match (=), then longest prefix; if that prefix has ^~, stop; otherwise try regexes in file order; first match wins; otherwise fall back to longest prefix.
  • “What’s the difference between ^~ and ~?”^~ is a prefix modifier meaning “if this prefix is the longest match, skip regex search.” ~ is a regex operator (case-sensitive). They serve different jobs.
  • “How do you make /static/ always serve from disk even though there’s a ~ \.(jpg|png)$ location?” — use ^~ /static/ so the prefix wins and regexes are skipped.
  • “Why might a request for /foo go to a different location than you expect?” — most often: a regex location matches, overriding what looked like the obvious prefix match. Add ^~ to lock the prefix.
  • root vs alias?”root appends the request URI to the path; alias replaces the location prefix. Trailing slash on alias matters.
  • “What’s a named location?”location @name, never matches a URI directly; used as a target for try_files and error_page internal redirects.