dnscache

dnscache provides a local DNS cache that is safe for concurrent use, bounded in size, and uses single-flight request collapsing to avoid duplicate lookups.

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

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

Package dnscache provides a local DNS cache that is safe for concurrent use, bounded in size, and uses single-flight request collapsing to avoid duplicate lookups.

It exposes Cache.LookupHost and Cache.DialContext, a drop-in replacement for an http.Transport DialContext.

Caching

Resolved host names are cached for one cache-wide TTL; the authoritative DNS record TTLs are not consulted. Concurrent callers asking for the same host share a single lookup.

Host names are matched case-insensitively and an equivalent trailing dot (FQDN form) is ignored, so “Example.com”, “example.com” and “example.com.” share a single entry. A custom Resolver therefore observes the normalized name. Host names that are already IP literals bypass the resolver and the cache entirely, mirroring net.Resolver.LookupHost.

The configured size bounds the number of cached address sets, never the number of distinct hosts looked up; Cache.Len can exceed it.

Dialing

Cache.DialContext dials the resolved IPs sequentially (not raced) in the resolver’s preference order, interleaving address families (leading with the resolver-preferred one) so a dead family is not exhausted before the other is tried. Family-restricted networks (“tcp4”, “udp6”, …) dial only addresses of the matching family.

WithDialer configures the dialer, WithDialTimeout bounds each attempt, and WithAddressRotation spreads connections across a host’s records.

Stale-if-error

WithStaleOnFailure serves the last successfully resolved addresses for a window measured from the failed refresh, so rarely resolved hosts are protected too. WithStaleIfError is the RFC 5861 variant, whose window is measured from the addresses’ original expiry.

When To Use

  • A client dials the same hostnames repeatedly and resolver latency is measurable.
  • Concurrent lookups of the same name should collapse into one query.

Example

// Create a DNS cache holding up to 128 hosts for 5 minutes each,
// using the default net.Resolver.
cache := dnscache.New(nil, 128, 5*time.Minute)

// Wire the cache into an http.Transport so every request reuses cached
// DNS resolutions instead of querying the resolver again.
client := &http.Client{
	Transport: &http.Transport{
		DialContext: cache.DialContext,
	},
}

_ = client // use the client as usual

fmt.Println(cache.Len())

// Output: 0

Full source is in example_dnscache_test.go. More runnable examples are on pkg.go.dev.

Dependencies

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