Skip to content
CloudOps
Newsletter
All guides
AI for NGINX By James Joyner IV · · 11 min read

Understanding NGINX Location Block Precedence With AI

Decode NGINX location and regex precedence with AI: exact, prefix, ^~, ~ and ~* order, why a URI hits the wrong block, and try_files, validated by nginx -t.

  • #nginx
  • #ai
  • #routing
  • #regex

Last week a teammate pinged me at 6 p.m. because /api/v2/Users/export.json was returning the SPA’s index.html instead of hitting the upstream. The config had eight location blocks, three of them regex, and everyone on the team had a different theory about which one was winning. The fix took two minutes once we understood the matching order. Understanding why took the rest of the evening, because NGINX location precedence is one of those rules people think they know until production proves otherwise.

This is exactly the kind of problem AI is good at: it can read a block of config, simulate the matching cascade for a specific URI, and explain in plain language which block fires and why. It will not run nginx -t for you, and it cannot see your live upstreams. So treat it as a very fast junior engineer who has memorized the rulebook. You stay in control, you validate, you reload.

The rule NGINX actually follows

The order most people think NGINX uses is “top to bottom, first match wins.” That is wrong. NGINX evaluates location blocks in a fixed priority order that has nothing to do with their position in the file:

  1. Exact match = — if the URI matches exactly, NGINX stops immediately.
  2. Prefix match with ^~ — longest matching prefix that uses ^~ wins, and regex is skipped entirely.
  3. Regex ~ (case-sensitive) and ~* (case-insensitive) — evaluated in file order, first regex to match wins.
  4. Plain prefix — the longest matching plain prefix is remembered as a fallback, used only if no regex matched.

The trap is step 3 versus step 4. A plain prefix can be the longest match in the file and still lose to a regex that appears anywhere, because regex outranks ordinary prefixes. That is precisely why my teammate’s request fell through.

Here is a config that exercises every case:

server {
    listen 80;
    server_name app.example.com;
    root /var/www/app;

    # 1. Exact match — wins outright, nothing else considered
    location = /healthz {
        return 200 "ok\n";
    }

    # 2. ^~ prefix — if longest, skips regex entirely
    location ^~ /assets/ {
        try_files $uri =404;
    }

    # 3. Case-sensitive regex
    location ~ \.(php)$ {
        return 403; # no PHP here, block it
    }

    # 3. Case-insensitive regex
    location ~* \.(json|xml)$ {
        proxy_pass http://api_upstream;
    }

    # 4. Plain prefix — fallback only
    location /api/ {
        proxy_pass http://api_upstream;
    }

    # 4. Plain prefix — the SPA catch-all
    location / {
        try_files $uri $uri/ /index.html;
    }
}

Walking a URI through the cascade

Take the original offender, /api/v2/Users/export.json. Watch the order:

  • = /healthz? No exact match.
  • ^~ /assets/? Does not start with /assets/. Skip.
  • Plain prefixes get remembered: /api/ matches (longer), / matches. NGINX holds /api/ as the best prefix candidate but does not commit yet.
  • Now regex, in file order: ~ \.(php)$? No. ~* \.(json|xml)$? Yes — the URI ends in .json. Regex wins. The remembered /api/ prefix is discarded.

In this specific case both the regex and the /api/ prefix point at api_upstream, so it happens to work. But flip the scenario: imagine that ~* \.(json|xml)$ block served static fixtures from disk instead of proxying. Then your dynamic API export silently returns a stale file, no error, no log line that screams “wrong block.” That is the failure mode that eats an evening.

The fix in my real case was to make the API path immune to regex by promoting it with ^~:

    location ^~ /api/ {
        proxy_pass http://api_upstream;
    }

With ^~, once /api/ is the longest matching prefix, NGINX commits and never evaluates the regex blocks. The .json URI now lands on the upstream regardless of extension.

Where try_files quietly changes the outcome

Precedence picks the block. try_files decides what happens inside it, and it is the second place requests go sideways. In the SPA catch-all:

    location / {
        try_files $uri $uri/ /index.html;
    }

NGINX tries the literal file, then the directory, then falls back to /index.html. That last fallback is why an unmatched /api/... request can end up serving the SPA shell with a 200 instead of a clean 404 — it matched the / prefix and try_files handed back index.html. If you ever see API calls returning HTML, check both which location won and whether a try_files fallback rewrote the response. The two-step nature is the part people miss.

Letting AI narrate the match

This is where I actually reach for a model. Paste the server block and ask it to trace a URI. A prompt that works well:

You are an NGINX expert. Given the server block below, simulate how NGINX selects a location block for the request URI /api/v2/Users/export.json. Evaluate in NGINX’s real priority order: exact =, then ^~, then regex ~/~* in file order, then longest plain prefix as fallback. State which block wins, name every block you skipped and why, and flag any try_files fallback that could change the final response. Do not assume top-to-bottom matching.

The “do not assume top-to-bottom” line matters — without it, models sometimes regurgitate the first-match-wins folk wisdom. A good response reads like this:

Selected block: location ~* \.(json|xml)$. Trace: = /healthz no match → ^~ /assets/ prefix doesn’t apply → plain prefixes /api/ and / are recorded as fallbacks but deferred → regex evaluated in order: ~ \.(php)$ no, ~* \.(json|xml)$ matches on .json. Regex outranks the recorded /api/ prefix, so /api/ is discarded. Risk: if you intended /api/ to win, promote it to ^~ /api/ so regex is skipped. No try_files fallback applies here since the regex block proxies directly.

That is a complete, correct trace. But notice the model is reasoning over text — it cannot know that api_upstream resolves, that the upstream is healthy, or that your DNS resolver is configured. AI explains the routing logic; it does not test the running system. Same discipline applies when you go a level deeper into hardening — see reviewing NGINX security configuration with AI for using a model to catch header and TLS mistakes the same way.

Validate, then reload — every time

No AI output goes live unverified. The model’s trace is a hypothesis. nginx -t is the test:

# Syntax and semantic check against the actual config tree
sudo nginx -t

# Expected:
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

# Only after a clean test, reload with zero dropped connections
sudo nginx -s reload

nginx -t catches the structural mistakes — a missing semicolon, an undefined upstream, an invalid regex — but it will not tell you that your precedence is wrong. A config can pass the test and still route /api/... to the SPA. So pair the syntax check with a real request against the running server:

# Confirm the JSON path actually hits the upstream, not index.html
curl -sS -o /dev/null -w '%{http_code} %{content_type}\n' \
  http://app.example.com/api/v2/Users/export.json

# A JSON content-type means the upstream answered.
# text/html means you hit the SPA fallback — precedence bug.

That curl is the validation that closes the loop. The model told you which block should win; the live request proves which block did.

How I actually use this on a team

The workflow that stuck for us: when someone touches location blocks, they paste the diff plus two or three representative URIs into a model and ask for a per-URI trace before opening the PR. The reviewer reads the trace, not just the config — it surfaces “this regex now shadows your API prefix” far faster than a human scanning eight blocks at 6 p.m. Then nginx -t and a curl smoke test gate the merge. The AI compresses the understanding step; the human owns the commit step.

If you want a starting library, I keep my NGINX-tracing prompts in the prompts collection, and the rest of the routing and proxy write-ups live under the NGINX category. Build your own prompt around the four-tier rule — exact, ^~, regex in order, longest prefix — and make the model explain the skips, not just the winner. The skipped blocks are where the bugs hide.

The rule itself is short enough to memorize: = beats ^~ beats regex beats plain prefix, regex ties break by file order, and try_files can still rewrite the answer underneath you. AI’s job is to apply that rule faster than you can in your head against a forty-line server block. Your job is to never let it reload the config on your behalf.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.