DNS Resolution Bug: Why Your fetch() Randomly Takes 5 Seconds

fetch sometimes 50ms, sometimes 5 seconds. Not a slow API - DNS timeout on IPv6. Node.js uses getaddrinfo AF_UNSPEC, tries IPv6 first, 5-second timeout, then falls back to IPv4.

· · 9 min read

I once added an external API endpoint to a production service. Simple fetch call. Just GET /api/price. In development: 50ms. Production: sometimes 50ms, sometimes 5 seconds. No pattern, no error, just random slowness.

I checked everything: network latency, server location, API rate limits. All normal. When I tested with curl, it was always 50ms. But fetch in Node.js would sometimes take 5 seconds.

3 hours of debugging later, I found it: it's DNS. Not the API, not the network. Node.js resolves DNS differently from curl. And that difference causes 5-second latency.

This is a problem rarely discussed, but the impact is significant. If you have services using fetch/axios to external APIs, you might be experiencing this without realizing it - you think "the API is slow."

---

The Case: Node.js fetch vs curl

curl - always 50ms
time curl -s https://api.exchange.com/v1/price

Node.js fetch - sometimes 50ms, sometimes 5 seconds
node -e "fetch('https://api.exchange.com/v1/price').then(r => r.text())"

Why the difference? Because curl and Node.js use different DNS resolvers:

• curl: uses getaddrinfo (system resolver - C library, also used by browsers)
• Node.js: uses dns.lookup by default, which has different behavior
• Node.js fetch (undici): uses dns.resolve - a pure JavaScript implementation

This implementation difference is what causes the timing degradation.

---

Root Cause: IPv6 vs IPv4

Many cloud servers (AWS, GCP, DigitalOcean) have dual-stack networking - supporting both IPv4 and IPv6. But often the IPv6 is not properly configured.

Node.js dns.lookup defaults to the system DNS resolver. But undici (which fetch uses in Node.js 18+) uses its own dns.resolve - and dns.resolve uses getaddrinfo with AF_UNSPEC hint, meaning "try IPv6 first, then IPv4."

If your server has IPv6 with a slow timeout (e.g., due to imperfect dual-stack), the resolver waits for the IPv6 timeout before falling back to IPv4. Default timeout: 5 seconds.

So:

1. fetch('https://api.exchange.com')
2. DNS resolve → tries IPv6 first → lookup timeout (5 sec)
3. Fallback to IPv4 → fast resolve (20ms)
4. Connect via IPv4 → request succeeds

Total: 5 seconds. But not every request hits this - sometimes IPv6 is fast (cached), sometimes slow. That's why it's intermittent.

---

How to Reproduce

const dns = require('dns')

// Default (AF_UNSPEC - tries IPv6 first)
console.time('resolve')
dns.resolve('api.exchange.com', (err, addresses) => {
console.timeEnd('resolve')
console.log('Addresses:', addresses)
})

// IPv4 only
console.time('resolve4')
dns.resolve4('api.exchange.com', (err, addresses) => {
console.timeEnd('resolve4')
console.log('IPv4:', addresses)
})

Output:
resolve: 5023.456ms ← 5 seconds!
resolve4: 21.123ms ← 21ms

If you see resolve taking 5 seconds but resolve4 is fast - congratulations, you've found the dual-stack timeout bug.

---

How to Fix

Fix 1: Force IPv4 in fetch (undici).

const { setGlobalDispatcher, Agent } = require('undici')

const agent = new Agent({
connect: { family: 4 }
})
setGlobalDispatcher(agent)

// Now fetch is always 50ms
const res = await fetch('https://api.exchange.com/v1/price')

Simplest fix. Trade-off: your service will never use IPv6, even if it's fast.

Fix 2: Configure DNS at the OS level.

Linux: prioritize IPv4 in /etc/gai.conf
precedence ::ffff:0:0/96 100

This fixes it at the OS level - all applications (curl, Node.js, Python) benefit. Requires root access.

Fix 3: Ensure IPv6 is properly configured.

Check if IPv6 is working
ping6 google.com
curl -6 https://google.com

If timeout - disable IPv6
/etc/sysctl.conf
net.ipv6.conf.all.disable_ipv6 = 1

The architecturally correct fix, but hardest if you don't have infrastructure access.

Fix 4: DNS cache for external APIs.

const dns = require('dns')
const dnsCache = new Map()

async function cachedDnsLookup(hostname) {
if (dnsCache.has(hostname)) return dnsCache.get(hostname)
const addresses = await dns.promises.resolve4(hostname)
dnsCache.set(hostname, addresses)
setTimeout(() => dnsCache.delete(hostname), 5 * 60 * 1000)
return addresses
}

const addresses = await cachedDnsLookup('api.exchange.com')
const res = await fetch(https://${addresses[0]}/v1/price, {
headers: { Host: 'api.exchange.com' }
})

DNS cache = resolve once, skip lookup for 5 minutes. But you need to handle cache invalidation if the server's IP changes.

---

Testing: Verify the Fix

for i in {1..10}; do
time node -e "fetch('https://api.exchange.com').then(r => r.text())"
done

If any request takes > 1000ms - still affected.
If all requests are < 60ms after the fix - it was DNS.

---

Common Misconceptions

"Slow fetch means the API is slow." - Not necessarily. Test with curl first. If curl is fast but fetch is slow, it's DNS, not the API.

"This is only a Node.js problem." - Not quite. Python requests also uses getaddrinfo with AF_UNSPEC. The difference is Python has built-in DNS caching. Node.js doesn't.

"Using axios fixes it." - No. Axios uses the http module which also relies on dns.lookup. The root problem is the DNS resolver, not the HTTP client.

"Upgrading to Node.js 22 fixes it." - No. This is about system DNS configuration, not the Node.js version.

---

An Honest Closing

5 seconds of latency gone with one line of configuration. Not an API issue, not a network issue - just Node.js DNS resolver trying IPv6 first, with IPv6 timing out after 5 seconds.

What makes this bug hard to debug: it's intermittent. Sometimes fast, sometimes slow. No error message, just "slow." You think the API is slow, but the problem is on your side.

The philosophy I learned: every layer in the stack can be a bottleneck - including DNS, which you rarely think about. You optimize database, cache, code - but DNS silently degrades performance.

If you experience intermittent API call slowness, don't panic. Test it: curl vs fetch. If curl is fast but fetch is slow, you know where to start looking.

---

Sources

• Node.js Documentation: DNS
• MDN Web Docs: Fetch API