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 53

You 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.

  • Performance
  • Code Quality
  • System administration
Jerome Musialak

Jerome Musialak

CEO @ Enodo

A passionate developer since the age of 11, Jérôme cut his teeth at MinuteBuzz and then as CTO of MeltyGroup, where he managed the technical infrastructure of around thirty high-traffic sites including Virgin Radio, L'Étudiant, ... and the group's media. With this experience and aware of the recurring challenges faced by companies in creating and effectively distributing their content, he founded Enodo (from the Latin "to untie knots") with the mission of simplifying the digital ecosystem. An expert in performance optimization and high-availability architecture, he puts his obsession with technical details to the service of business challenges to build reliable systems that allow everyone to sleep soundly.

On the same subject