Server

How Nginx location Block Matching Actually Works

By Prabath Thalangama· October 24, 2026· 3 min read
#nginx#troubleshooting

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:

  1. Exact match (=) — if one matches, use it and stop. Done.
  2. Prefix matches — find the longest matching prefix location. Remember it.
  3. If that longest prefix has ^~, use it and stop — regex is skipped.
  4. Otherwise, check regex locations in the order they appear in the file. The first regex that matches wins and stops the search.
  5. 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 =). So location /images/ loses to location ~ \.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 static location, or put a location ~ \.php$ { ... } guard inside it.
  • try_files gives 404 for a file that exists — paths in try_files are relative to root/alias; a mismatched alias (which replaces, not appends) is the usual cause. alias needs a trailing slash to match a location with one.
  • Everything hits the / location — your specific locations have a typo or the wrong modifier; nginx -T and read them top to bottom as Nginx would.
  • Config test passes, behaviour unchanged — you edited a file not included, or there's a server_name mismatch sending the request to a different server {} block. curl -H "Host: ..." and check nginx -T for the server that actually matches.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.