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.
Advanced
What you will learn
The anatomy: address,
valid=,ipv6=off— withresolver_timeoutas its own directive.The two contexts — runtime (this directive) vs config-load (the system resolver) — and the error that proves it.
Caching semantics:
valid=vs TTL, including failures.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:
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 |
| honors each record's TTL | how long ANY answer is cached — overrides TTL, up or down |
| 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
|
|---|---|---|---|
Config load (
| once, when config is parsed | the
system resolver —
| NO |
Runtime (variable
| per lookup, cached per
| NGINX's
internal async resolver — the
| YES |
A first
nginx -t fails with exactly this receipt:
[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
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 ownresolver(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 exercisevalid=caching from both sides: the flip follows within ~2s, the outage 502s only after cache expiry.Hand-run the gotcha: change the
dyn_poolserver line by removingresolve, runnginx -t— meet[emerg] host not found in upstreamyourself, then restore. (Five minutes, permanently unforgettable.)Watch the query traffic:
tail -f /var/log/b-dns.logwhile curling:9500in a loop — see how the query rate tracksvalid=, 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
resolverarms runtime resolution only — config-load lookups go through the system resolver and failnginx -tif 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 toresolver_timeoutper attempt — they don’t load-balance.ipv6=offwhen the fleet is IPv4-only: half the queries, zero AAAA stalls.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.