Nginx Configuration and Security

Location matching precedence

Nginx does not simply use the first matching location block in the file — it follows a fixed precedence order regardless of how the blocks are arranged:

graph TD req["Incoming request path"] --> exact{"Exact match?<br/>location = /path"} exact -->|"yes"| use1["Use it, stop searching"] exact -->|"no"| prefix{"Longest matching<br/>prefix location?"} prefix --> regex{"Any regex location<br/>(~ or ~*) matches?<br/>checked in FILE ORDER"} regex -->|"yes"| use2["Use first matching regex"] regex -->|"no"| use3["Fall back to the longest<br/>prefix match found earlier"]
  1. location = /exact/path — exact match, wins immediately if it matches, no further checking.
  2. Prefix matches (location /api/) — Nginx finds the longest matching prefix among all of them, not the first one written.
  3. Regex matches (location ~ \.php$ or ~* for case-insensitive) — checked in the order they appear in the config file; the first regex that matches wins, and regex matches take priority over a plain prefix match (but not over an exact match).
  4. If no regex matches, Nginx falls back to the longest prefix match found in step 2.

This ordering — not file order — is the single most common source of "why is my location block being ignored" confusion, since it's easy to assume Nginx just reads top to bottom like a firewall rule list.

Rate limiting

http {
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

    server {
        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;
        }
    }
}

limit_req_zone defines a shared-memory zone keyed by $binary_remote_addr (the client IP) tracking request rate — this is Nginx's own implementation of the leaky-bucket-shaped rate limiting already covered as a system-design case study, applied at the web server layer instead of an application-level Redis-backed limiter. rate=10r/s sets the steady-state limit; burst=20 allows up to 20 requests to queue briefly above that rate before being rejected (this is the token-bucket-style burst allowance from that same case study, not a hard per-second cutoff); nodelay serves burst-allowance requests immediately instead of artificially spacing them out to match the steady rate — without nodelay, requests within the burst allowance are still accepted but deliberately delayed to smooth the rate, which is sometimes the desired behavior and sometimes not, depending on whether smoothing or low-latency-within-budget matters more for the specific endpoint.

gzip and security headers

gzip on;
gzip_types text/plain application/json text/css application/javascript;
gzip_min_length 1024;

add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

gzip_min_length avoids compressing tiny responses where compression overhead outweighs the bandwidth saved. The security headers are cheap, high-value defaults: X-Content-Type-Options: nosniff stops browsers from guessing content types in a way that can enable certain attacks; X-Frame-Options: DENY prevents the page from being embedded in an iframe (clickjacking mitigation); Strict-Transport-Security tells the browser to only ever connect over HTTPS for this domain going forward, closing the window for a downgrade attack on subsequent visits.

TLS termination

Nginx commonly terminates TLS at the edge (handles the TLS handshake itself, decrypts incoming requests) and forwards plain HTTP to backend servers on a trusted internal network — moving the CPU cost of encryption to one place and simplifying certificate management to one location instead of every backend instance needing its own certificate.

Common pitfall

Assuming limit_req's burst parameter means "allow this many extra requests per second" rather than "allow a queue of up to this many requests beyond the steady rate before rejecting" is a common misread — burst=20 with rate=10r/s doesn't mean 20 extra requests/second forever, it means a client bursting past 10r/s can have up to 20 requests queued (and, with nodelay, served immediately) before further excess requests start getting 503'd, and that budget refills at the steady rate over time, not once per second flatly.