Nginx

Nginx Location Block Priority, Why the Wrong One Matches

When a request gets handled by an unexpected location block despite a seemingly more specific one existing in the config, the cause is almost always a misunderstanding of Nginx's actual matching priority — it isn't simply "top to bottom" or "most specific wins" in the way many developers initially assume.

The Problem

You define what looks like a more specific location block for a particular path, but requests to that path are instead handled by a different, more general block elsewhere in the config, ignoring the one you expected to match.

Nginx location matching order 1. Exact match: location = /path 2. Preferential prefix: location ^~ /path 3. Regex, first match wins: location ~ or ~* 4. Longest prefix match: location /path (checked LAST)

Why It Happens

Nginx doesn't process location blocks strictly in the order they appear in the file, nor does it always prefer the most specific-looking path. Instead, it follows a specific priority order based on the type of match:

  • Exact matches (location = /path) are checked first and, if found, win immediately regardless of any other block
  • Preferential prefix matches (location ^~ /path) are checked next — if the request path starts with this prefix, Nginx stops checking regex locations entirely for this request
  • Regular expression locations (location ~ /pattern for case-sensitive, location ~* /pattern for case-insensitive) are checked in the order they appear in the file, and the first one that matches wins
  • Plain prefix matches (location /path with no special prefix) are actually evaluated last among these categories, and among multiple matching plain prefixes, the longest one wins — but only after none of the higher-priority categories above matched first

The common misconception is assuming plain prefix locations are evaluated top-to-bottom with the first match winning — in reality, Nginx compares all plain prefix locations and picks the longest matching one, but only considers that category at all after exact and regex matches have already been ruled out.

The Fix

To make a specific path win over more general regex or prefix matches, use an exact match if the path is a single fixed route:

location = /health {
    return 200 "OK";
}

For a prefix that should win over any regex locations without needing to be an exact single path, use the preferential prefix marker:

location ^~ /static/ {
    root /var/www/assets;
}

This tells Nginx: if a request path starts with /static/, stop checking regex locations entirely and use this block, even if a regex location elsewhere in the file would have also matched.

If you're relying on regex locations, remember their priority is determined by file order, not specificity — a broader regex defined earlier in the file wins over a more specific one defined later, if both would technically match the same request:

# This broad pattern, if listed first, wins over the more specific one below
location ~ \.php$ { ... }
location ~ ^/api/.*\.php$ { ... }  # never reached for matching requests, since the above already matched

Reordering regex locations so more specific patterns appear before broader ones (when both could match the same request) ensures the intended, more specific block actually gets used.

To debug which location block is actually handling a given request, add a temporary custom header identifying the block, and inspect it in the response:

location /api/ {
    add_header X-Matched-Location "/api/";
    # ... rest of config
}
curl -I https://your-domain.com/api/test

Seeing this custom header in the response confirms exactly which block handled the request, removing any ambiguity about Nginx's actual matching decision.

Still Not Working?

If the matching behavior still seems inconsistent with the documented priority rules above, check whether the location blocks are spread across multiple included config files rather than a single visible file, since Nginx's include directive can make the effective order of blocks less obvious than it appears when reading just one file in isolation. Use nginx -T to dump the fully resolved configuration, with all includes expanded, to see the actual complete picture Nginx is working from:

sudo nginx -T | less