dnscache

dnscache is a bounded, single-flight DNS cache with a DialContext drop-in for http.Transport, interleaving address families and rotating records.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/dnscache"

Caches host resolution and dials through it. Cache.DialContext drops straight into an http.Transport:

c := dnscache.New(nil, 1024, 30*time.Second)

transport := &http.Transport{DialContext: c.DialContext}

Why Go programs need this

Go’s resolver does not cache. Every LookupHost goes to the configured DNS servers, and in a container that usually means a cluster DNS service a network hop away, sometimes with a search-domain list that turns one lookup into four or five queries.

A service making outbound calls at any volume therefore generates continuous DNS traffic, and the resolver becomes a dependency on the hot path of every request that opens a connection. Cluster DNS falling over is a common and unpleasant failure, and it is what this package is for.

TTL and normalisation

One cache-wide TTL. Authoritative record TTLs are not consulted.

That is a deliberate simplification with a real consequence: a record with a 30-second TTL is held for your configured duration regardless. Set the TTL short enough for your failover expectations, since it directly bounds how long the service keeps dialling an address that has moved.

Concurrent callers asking for the same host share a single lookup, the same single-flight property as sfcache. A burst of requests to a newly-referenced host produces one query.

Host names are matched case-insensitively and a trailing dot is ignored, so Example.com, example.com and example.com. share one entry. A custom Resolver sees the normalised name. Hosts that are already IP literals bypass the resolver and the cache entirely, matching net.Resolver.LookupHost.

One stated subtlety: the size bounds the number of cached address sets, never the number of distinct hosts looked up, so Cache.Len can exceed the configured size.

The dialing behaviour is where the design shows

DialContext does more than resolve and connect.

Addresses are dialled sequentially, in the resolver’s preference order. Not raced. Racing wastes connections and load on the far side.

Address families are interleaved, leading with the resolver-preferred one. This is the important part. A host with four IPv6 and four IPv4 addresses, in an environment where IPv6 is broken, would otherwise burn four timeouts before reaching an address that works. Interleaving means the second attempt already crosses to the other family.

Family-restricted networks are honoured. tcp4 dials only IPv4 addresses.

WithDialTimeout bounds each attempt. Without a per-attempt bound the interleaving is useless, because one hanging address consumes the whole dial budget.

WithAddressRotation spreads connections across a host’s records, so a multi-address host gets balanced rather than every connection landing on whichever address is listed first.

WithDialer supplies your own net.Dialer for keep-alive and local address settings.

Stale-if-error

A failed refresh can serve the previous addresses rather than failing the dial. During a DNS outage the service keeps talking to the endpoints it already knew about, which are usually still there: DNS being down rarely means the hosts moved.

Cache management

Len, Reset, Remove and PurgeExpired are available. Remove is the escape hatch for a planned failover where you know an address changed and do not want to wait out the TTL.

Dependencies

This package reaches no external module: it uses only the Go standard library.