DNS Resolution Patterns
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
- Lookup with 1ms timeout—handle error.
- Compare
LookupIPvsLookupCNAME.
- Force custom DNS Dial to an internal resolver.