Node.js: How DNS Lookups can block your performance
While working on the next version of a Node.js application for a client, we discovered that DNS lookups could become a hidden bottleneck, slowing down a critical layer of the infrastructure. Fortunately, the fix is simple.
When we talk about Node.js performance, we immediately think of the event loop, non-blocking code, application caching, or SQL query optimization.
Yet, while working on a high-traffic Node.js application for a client, we identified a much more subtle bottleneck capable of slowing down a critical layer of the infrastructure: DNS lookups.
A problem that is rarely monitored, often misunderstood, yet very real in production.
How Node.js handles DNS
For every outgoing HTTP request (third-party API, internal microservice, monitoring tool, SaaS service…), Node.js needs to resolve a domain name into an IP address. To do this, it relies on dns.lookup(), which uses the operating system’s resolver and does not implement any DNS caching at the application level.
Result: each HTTP request can trigger a new DNS lookup, even if the domain is the same and called multiple times per second.
Why This Is a Real Performance Issue
1. DNS lookups use the libuv thread pool
Contrary to what one might think, DNS resolution is not handled by the main event loop.
It goes through the libuv thread pool, which contains 4 threads by default.
Concretely:
- 1 DNS lookup = 1 occupied libuv thread
- 4 simultaneous lookups = pool saturation
- other operations (DNS, fs, crypto, I/O) must wait
Under load, this can cause:
- increased latency
- intermittent timeouts
- errors that are hard to reproduce
2. The OS DNS cache is not enough
One might assume the OS DNS cache solves the problem. In practice:
- it is global, shared between all processes
- it often has a short TTL
- it is not designed to handle bursts of backend requests
In a Node.js backend, multiple concurrent requests can trigger identical DNS lookups in parallel, even before the OS cache is leveraged.
Real Example: An External Service Reveals the Problem
By observing DNS traffic with:
tcpdump -vvv -n -i any udp port 53You can see numerous DNS requests to third-party services (monitoring, analytics, external APIs), often in A/AAAA pairs, repeated multiple times per second.
The problem is not the external service.
The problem is that Node.js performs a DNS lookup for every HTTP request.
Possible solutions... and their limits
Modify /etc/hosts
Coding the IP directly into /etc/hosts allows instant resolution.
Advantages:
- immediate lookup
- zero DNS requests
Disadvantages:
- static IP
- breaks if infrastructure changes
- complex maintenance
- unsuitable for scalable production
Use an optimized DNS resolver (Unbound)
A resolver like Unbound is a great infrastructure component:
- local DNS cache
- very low latency
- reduced calls to public DNS
- better resilience
But… Node.js still calls dns.lookup() for every HTTP request.
An optimized resolver speeds up DNS, but does not reduce the number of lookups or relieve pressure on the libuv thread pool.
The real solution: Application-Level DNS Caching
To fix the problem at its source, you need to act at the application level.
The simplest and cleanest solution is to use cacheable-lookup.
This library:
- intercepts outgoing HTTP requests
- caches DNS resolutions in memory
- respects DNS TTLs
- avoids repetitive lookups
6 lines of code to fix the problem
import http from 'node:http';
import https from 'node:https';
import CacheableLookup from 'cacheable-lookup';
const cacheable = new CacheableLookup();
cacheable.install(http.globalAgent);
cacheable.install(https.globalAgent);
From there:
- a domain is resolved only once
- libuv threads are freed
- latency becomes more stable
- timeouts drop drastically
Production benefits
- Faster APIs
- Better scalability under load
- Less pressure on libuv thread pool
- Fewer network errors
- Reduced unnecessary DNS calls
Best Practices
- Respect DNS TTLs
- Test under load before production
- Combine application-level DNS cache + optimized DNS resolver for a robust infrastructure
Conclusion
Node.js performance is not only about JavaScript.
It also depends on the invisible:
- DNS
- libuv thread pool
- I/O
- cache
Sometimes, a few lines of code are enough to eliminate a critical bottleneck and prevent serious production issues.