DNS Resolution Patterns

Updated

September 8, 2026

DNS Resolution Patterns

Overview

Every dial starts with a name. Bad DNS assumptions cause mysterious latency and sticky black holes. Control resolvers, dual-stack, and caching consciously.

Default resolver

ips, err := net.DefaultResolver.LookupIPAddr(ctx, "example.com")

Always pass context with timeout.

Custom resolver

r := &net.Resolver{
    PreferGo: true,
    Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
        d := net.Dialer{Timeout: 2 * time.Second}
        return d.DialContext(ctx, "udp", "1.1.1.1:53")
    },
}
ips, err := r.LookupIPAddr(ctx, host)

Dialer and Happy Eyeballs

d := net.Dialer{Timeout: 5 * time.Second, KeepAlive: 30 * time.Second}
conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, "443"))

Go’s dialer handles much of dual-stack; still set overall timeouts.

Caching

net.Resolver does not give you full app-level cache control. For high-QPS:

  • Prefer connection reuse (HTTP Transport) over re-resolve every call
  • Short-lived process caches with TTL if you roll your own (invalidate!)

Failure modes

Symptom Cause
Sporadic dial timeout Resolver/network blip
Sticky bad IP Long-lived conn to dead backend; need health + re-resolve
IPv6 blackhole Broken AAAA path

Minimal dig-like

for _, ip := range ips {
    fmt.Println(ip.IP)
}

(See CLI netkit dns command.)

Rules of thumb

Do Don’t
ctx timeout on Lookup Block forever on DNS
Re-resolve on new connections Assume one IP forever for dynamic backends
Log resolved IP on errors Only log hostname

Try next

  1. Lookup with 1ms timeout—handle error.
  2. Compare LookupIP vs LookupCNAME.
  3. Force custom DNS Dial to an internal resolver.