Node.js DNS resolution isn’t just a simple wrapper around the OS’s getaddrinfo; it’s a sophisticated, multi-layered system that prioritizes speed and efficiency through aggressive caching and a configurable fallback mechanism.

Let’s see it in action. Imagine you’re building a web scraper and need to resolve a domain.

const dns = require('dns');

// Resolve a domain name to an array of IP addresses
dns.lookup('example.com', (err, address, family) => {
  if (err) {
    console.error('DNS lookup failed:', err);
    return;
  }
  console.log(`example.com resolved to ${address} (IPv${family})`);
});

// Resolve a domain name to a CNAME record
dns.resolve('www.google.com', 'CNAME', (err, addresses) => {
  if (err) {
    console.error('CNAME resolution failed:', err);
    return;
  }
  console.log(`www.google.com CNAME record:`, addresses);
});

// Resolve a domain name to an MX record
dns.resolve('gmail.com', 'MX', (err, addresses) => {
  if (err) {
    console.error('MX resolution failed:', err);
    return;
  }
  console.log(`gmail.com MX records:`, addresses);
});

When you run this, Node.js doesn’t immediately ask your operating system for the IP address. Instead, it first consults its own internal DNS cache. This cache is populated by previous successful (and sometimes failed) lookups. If a match is found and the record hasn’t expired (based on TTL, Time To Live), Node.js returns the cached result instantly, bypassing the network entirely. This is a massive performance win for applications making frequent DNS requests to the same domains.

If the DNS entry isn’t in the cache, or has expired, Node.js then uses the dns.lookup function. This function, by default, uses the operating system’s underlying getaddrinfo C library call. This is where the actual network query happens if the OS doesn’t have it cached either. getaddrinfo is powerful; it queries your system’s configured DNS servers (usually found in /etc/resolv.conf on Linux/macOS or via network settings on Windows) to find the IP address. It also handles name resolution order (e.g., checking hosts files before DNS servers if configured).

The dns.resolve family of functions (resolve, resolve4, resolve6, resolveMx, resolveTxt, etc.) behave differently. They always perform a network query and do not use the OS’s getaddrinfo mechanism. Instead, they directly interact with DNS servers using UDP or TCP, querying for specific record types (A, AAAA, MX, TXT, etc.). This gives you granular control over what information you retrieve and bypasses some of the OS-level interpretation that lookup does, making it ideal for inspecting DNS records directly.

The mental model here is a tiered approach: Node.js internal cache (fastest) -> OS getaddrinfo (uses OS cache and network) -> Direct DNS server queries via resolve functions (always network). You control the behavior through the dns module’s API. For instance, dns.setServers() allows you to explicitly define which DNS servers Node.js should use for its resolve queries, overriding system defaults. You can also configure lookup behavior.

The most surprising thing about dns.lookup is that it doesn’t necessarily perform a network request at all. It consults the operating system’s name resolution service, which includes files like /etc/hosts and potentially NIS or other name service switch (NSS) modules before ever hitting a DNS server. This means a seemingly network-bound operation can be satisfied entirely locally, and it’s why dns.lookup might return an IP address for localhost even if your DNS servers are down.

The next hurdle you’ll encounter is handling DNS errors gracefully, as network issues or misconfigurations can lead to ENOTFOUND, ETIMEDOUT, or ECONNREFUSED errors.

Want structured learning?

Take the full Nodejs course →