skip to content

Does Go's net resolver cache DNS answers between lookups, and does it honour record TTLs?

level: middleimportance: nice to knowfreq 28%

answer

  1. fresh query every time
  2. the API never shows you a TTL
  3. the config is remembered, the answers are not
  4. a failover is picked up immediately
  5. any cache you see belongs to the host

basics

~20 s

No. Go's own DNS resolver keeps no answer cache and does not expose or retain record TTLs, so every lookup queries again. Any caching you observe comes from the host's resolver stack when the C path is in use, not from Go.

solid answer

~50 s

Go's built-in resolver performs a fresh query for every lookup. There is no process-wide answer cache, the `net` API never surfaces a TTL, and nothing in the standard library remembers a previous answer. What Go does keep in memory is the parsed `/etc/resolv.conf`, and even that is re-checked so a change to the file is picked up by a running process rather than requiring a restart. When resolution goes through the C library instead, whatever the host caches — a local caching resolver or name-service cache — applies, which means the *same binary* can look cached on one machine and uncached on another. Engineers arriving from runtimes that cache by default are usually surprised twice: first by the query volume, then by discovering that Go picks up a DNS failover almost immediately because it never held a stale answer.

go deeper

for a junior

Remember the plain fact: Go does not cache name lookups, so each call goes back to the nameserver. Do not assume a repeated lookup is free.

for a middle

Be ready to say what is cached instead — the parsed resolver configuration, re-checked while the process runs — and that any answer caching you observe belongs to the host's resolver stack.

for a senior

Show both sides: the query volume a busy service puts on a shared resolver, and the benefit that a DNS failover takes effect immediately because no stale answer is held anywhere in the process.

for a principal

Decide where caching belongs. Argue for a caching resolver next to the workload, operated by the people who operate DNS, over per-service in-process caches with invented expiry times.

## The short answer, and why it surprises people Go's own DNS resolver does not cache. Ten sequential lookups of the same hostname send ten sets of queries to the configured nameservers. There is no per-process answer cache, no negative cache, and the API does not even expose the TTL from the response, so an application cannot honour it without doing its own DNS. This catches out engineers coming from runtimes where the platform caches name lookups by default and the interesting question is how to *tune* the cache. In Go the interesting question is the reverse: whether you need caching at all, and where you would put it. ## What Go does keep One thing is cached, and it is configuration rather than answers: the parsed contents of `/etc/resolv.conf`. Go reads that file to learn its nameservers and options, holds the parsed form, and re-checks it periodically so that an edit made while the process is running is picked up without a restart. That is a deliberate behaviour and worth knowing, because it means a long-running process is not permanently bound to the resolver configuration that existed at start-up. When there is no usable `/etc/resolv.conf` at all — a minimal container image that never included one — the Go resolver falls back to trying a nameserver on the loopback address. In an image with nothing listening there, every lookup fails, and it fails in a way that looks like a broken network rather than missing configuration. ## Where caching actually happens If you see caching behaviour from a Go program, it is coming from underneath it. When the lookup is served by the C library, the host's name-service stack is in play, and that stack may well have a cache. Whether it does is a property of the machine, not of your binary. That is why the caching question and the resolver-selection question are entangled: pinning Go's own resolver also, silently, opts you out of the host's cache. ## The consequences worth naming in an interview **Query volume.** A service that resolves a hostname on every unit of work sends a query for every unit of work. On a busy service that can be a meaningful load on a shared resolver, and resolvers do get rate-limited or overloaded. **Failover latency, in your favour.** Because nothing holds an old answer, a DNS change — a failover that repoints a name at a different address — is picked up at the next lookup. Systems that cache aggressively and ignore short TTLs are the ones that keep hammering a dead address after a failover. Go's lack of caching is a real operational advantage here, and it is worth saying so rather than treating no-cache as purely a cost. **Environment-dependent behaviour.** The same binary behaves differently depending on which resolver is in use and what the host caches. Two deployments that differ only in base image can differ in query volume by orders of magnitude. ## If you decide you need a cache The options, roughly in order of how much you should like them: 1. **Run a caching resolver next to the workload** — on the host or as a sidecar — and point `/etc/resolv.conf` at it. The cache then honours TTLs properly, is shared by every process on the machine, and is operable by the people who operate DNS. Your Go code does not change. 2. **Let the C resolver serve lookups** where the host already provides caching, accepting the thread-cost and portability trade-offs that come with it. 3. **Cache in your application** — memoise resolved addresses behind your own expiry. This is the option that looks easiest and ages worst. You cannot see the record's TTL, so you invent one; a name that fails over is stale for as long as your invented interval; negative results need their own policy; and the behaviour is now invisible to whoever is debugging DNS from the outside. If you do it, keep the interval short, cache negatives separately or not at all, and expose the cache's state in metrics so it can be reasoned about during an incident. The honest summary for a design discussion: Go's default is *no cache, always fresh*, and the right place to add caching is the resolver layer that already understands TTLs, not a map in your process.

  • Where does caching happen when a Go binary resolves through the C library instead?
    In the host's own name-service stack, if that machine runs a caching resolver. It is a property of the environment, not of the binary, so the identical program can be cached on one host and uncached on another. Nothing inside the `net` package caches answers in either mode.
  • What are the risks of putting your own DNS cache in front of net.LookupHost?
    You cannot see the record's TTL, so you invent an expiry that has no relationship to what the zone owner intended. A failover then stays stale for your interval instead of theirs, negative results need a separate policy or they pin an outage in place, and the caching is invisible to anyone debugging DNS from outside the process. A caching resolver next to the workload solves it better.
  • Does a long-running Go process notice a change to /etc/resolv.conf?
    Yes. The Go resolver holds the parsed configuration but re-checks the file, so a nameserver change is picked up by the running process rather than needing a restart. That matters in environments where the file is rewritten under a running workload.

saying these in an interview costs you the question

  • Assumes Go caches lookups and honours the record TTL
  • Thinks a per-process answer cache is configurable somewhere in net
  • Cannot say where observed caching is actually coming from
  • Reaches straight for an in-process map with an invented expiry
  • Treats the absence of a cache as purely a drawback