Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

13.2 The Resolver Directive

One directive, three parameters, two completely different resolution contexts — and a config-load gotcha you can hit on your very first nginx -t. Know which resolver answers which name.

Levels: Advanced ~30 min read

Advanced

What you will learn

  1. The anatomy: address, valid=, ipv6=off — with resolver_timeout as its own directive.

  2. The two contexts — runtime (this directive) vs config-load (the system resolver) — and the error that proves it.

  3. Caching semantics: valid= vs TTL, including failures.

  4. Where the directive can live, and the inheritance rules.


1. Anatomy

The examples across this module share one small demo environment: a DNS server on 127.0.0.1:9053 answering *.example.test names (query log: /var/log/b-dns.log), a records API on :9054 that can change those records mid-flight, and front doors on :9500–:9502 proxying to the names under test — so every re-resolution story can be watched live:

nginx
resolver  127.0.0.1:9053  valid=2s  ipv6=off;

Parameter

Default

What it controls

address (required)

—

DNS server(s); repeatable for a list — nginx rotates, not load-balances

valid=

honors each record's TTL

how long ANY answer is cached — overrides TTL, up or down

ipv6=off

on

suppress AAAA queries — no IPv6 in your fleet, no reason to spend a second query per lookup

One subtlety worth knowing: the per-query wait is not a resolver parameter — it is its own directive, resolver_timeout (a name-resolution budget for the request path, default 30s, legal wherever resolver is). But valid= is the one you’ll tune: it IS your staleness dial. The examples in this module pin it at 2s so the effects move in seconds; production typically lands at 5–30s depending on churn (and DNS query volume — every expiry is a UDP round trip per name per worker-shared cache).

2. The two contexts (the classic first-run gotcha)

NGINX resolves names in two completely different machines:

Context

When

Who resolves

Sees resolver?

Config load ( nginx -t/start/reload)

once, when config is parsed

the system resolver — /etc/resolv.conf, /etc/hosts, nsswitch

NO

Runtime (variable proxy_pass, server … resolve, auth subrequest URLs, …)

per lookup, cached per valid=

NGINX's internal async resolver — the resolver directive

YES

A first nginx -t fails with exactly this receipt:

text
[emerg] host not found in upstream "svc.example.test:9001"

That upstream's server had no resolve parameter — so its name went through the config-load context, which uses the system resolver, which knows nothing about our example DNS at 127.0.0.1:9053. The resolver directive was right there in the same file and could not help: it only arms the runtime path. (This is also why a plain server api.github.com:443; upstream works on a machine with normal DNS but breaks in containers whose /etc/resolv.conf is restricted — different machine, same rule.)

The fix for a truly static name that only DNS knows: add resolve (Lesson 13.4) to move the name into the runtime context — or resolve it outside NGINX (a template, an alias) — or use the variable pattern (13.3), which is runtime-native.

3. Caching semantics — including failures

The runtime resolver caches answers per valid=, TTL notwithstanding. The demo DNS server hands out TTL=5; the valid=2s means NGINX re-asks every 2 seconds regardless. Set valid=300 and a 5-second TTL record will still be held for five minutes — sometimes exactly right (chatty upstreams, self-managed DNS), sometimes a trap (you asked for freshness and disabled it).

Failure caching follows the same clock: an NXDOMAIN or SERVFAIL isn't retried until the previous answer expires. Lesson 13.3 demonstrates that clock end to end — record deleted at T0, 200s while the cached answer holds, then honest 502s with could not be resolved in the error log the moment valid= expires — one dial, both effects, no separate knobs.

4. Placement and inheritance

nginx
http {
    resolver 127.0.0.1:9053 valid=2s ipv6=off;   # http-wide default

    server {
        resolver 10.0.0.5:53 valid=10s;          # override: THIS server only
        location / {
            resolver 127.0.0.1:9053 valid=1s;    # override again, finest grain
        }
    }
}
  • Legal at http, server, location — the nearest one wins; there is no merging of server lists between levels.

  • stream{} has its own resolver (same semantics, Module 10's world — UDP-proxying DNS frontends configure it too).

  • Multiple addresses are a round-robin, not a pool: one server per query, rotating — no health marking, no pooling; each query waits up to resolver_timeout (a separate directive, default 30s) before the attempt fails. For redundant DNS, the rotation is built in.

Try it yourself

  • curl -s http://127.0.0.1:9500/probe — poll it across a record flip and across a deletion to exercise valid= caching from both sides: the flip follows within ~2s, the outage 502s only after cache expiry.

  • Hand-run the gotcha: change the dyn_pool server line by removing resolve, run nginx -t — meet [emerg] host not found in upstream yourself, then restore. (Five minutes, permanently unforgettable.)

  • Watch the query traffic: tail -f /var/log/b-dns.log while curling :9500 in a loop — see how the query rate tracks valid=, not the request rate.

Q&A

Q: Why does valid= override TTL instead of using max/min? A: Design choice: the operator knows their fleet better than the zone's TTL author. If you WANT TTL behavior, simply omit valid= — NGINX then honors each record's TTL (that's the default column of the table).

Q: Is resolution on the request path? Does a DNS outage add latency? A: Only on cache expiry: the request that triggers a re-resolve waits up to resolver_timeout (default 30s) — one request per valid window, not every request. If DNS is down, that unlucky request pays the timeout; the rest fail fast on cached NXDOMAIN or the previous good answer.

Q: Can I use /etc/hosts names with the runtime resolver? A: No — the internal resolver speaks DNS (UDP/TCP) only. /etc/hosts lives in the config-load context's world. (In containers this distinction regularly surprises people.)

Q: resolver in upstream{} itself? A: resolve-bearing upstreams pick up the resolver from the context where the upstream is used (http/server/location inheritance) — since 1.27.3 resolver and resolver_timeout are valid inside the upstream block itself; otherwise the http-level resolver is inherited.

Takeaways

  • resolver arms runtime resolution only — config-load lookups go through the system resolver and fail nginx -t if it can't answer (our shipped receipt).

  • valid= is the staleness dial: it overrides TTL, caches failures on the same clock, and trades DNS query volume against how fast NGINX follows the fleet.

  • Nearest-level-wins placement (http/server/location, plus the stream{} sibling); multiple addresses rotate — one server per query, up to resolver_timeout per attempt — they don’t load-balance.

  • ipv6=off when the fleet is IPv4-only: half the queries, zero AAAA stalls.

Was this page helpful?

Mark this learning resource complete to track your learning progress.