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.
0xNN · · 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