NodeJSDNS Lookups (dns Module)

DNS Lookups (dns Module)

Before any socket can connect, a hostname like example.com must be turned into an IP address like 93.184.216.34. That translation is DNS (the Domain Name System), and Node's dns module exposes it. There's an important and frequently-misunderstood split here: dns.lookup (which uses the OS resolver) versus the dns.resolve* family (which queries DNS servers directly). Choosing wrong has real performance consequences.

The promise API

JS
import { lookup, resolve4, resolveMx } from 'node:dns/promises'

// Hostname → IP, the way connect() does it:
const { address, family } = await lookup('example.com')
console.log(address, 'IPv' + family)        // e.g. 93.184.216.34 IPv4

// Query specific record types directly from DNS:
console.log(await resolve4('example.com'))  // ['93.184.216.34', ...] all A records
console.log(await resolveMx('gmail.com'))   // mail servers with priorities
Use `node:dns/promises` for async/await
Like other core modules, `dns` has a callback API on `node:dns` and a promise API on `node:dns/promises`. Prefer the promise version. (`dns.lookup` also has a synchronous-feeling but still-async nature — see the threadpool warning below.)
lookup vs resolve — the key distinction

dns.lookup

dns.resolve*

Mechanism

OS resolver (getaddrinfo)

Direct DNS query (c-ares)

Honors /etc/hosts

Yes

No

Honors OS cache & config

Yes

No

Record types

A/AAAA only (IP for connecting)

A, AAAA, MX, TXT, SRV, NS, CNAME…

Runs on

libuv threadpool (blocking call)

Async network I/O (non-blocking)

Use for

Getting an IP to connect to

Inspecting DNS records

`dns.lookup` runs on the libuv threadpool — it can become a bottleneck
`dns.lookup` calls the OS's `getaddrinfo`, which is **synchronous and blocking**, so Node runs it on the libuv threadpool (default size **4**). Under heavy load — many outbound connections resolving at once — these calls can saturate the threadpool, delaying *other* threadpool work (file I/O, `crypto.pbkdf2`, zlib). `dns.resolve*` uses non-blocking network queries instead and doesn't touch the threadpool. For high-throughput clients, prefer `resolve*` or cache results.
Why `lookup` is still the default
Despite the threadpool caveat, `dns.lookup` is what `http`, `net`, and `connect` use by default — because it respects `/etc/hosts`, the OS cache, and platform-specific resolution rules (which `resolve*` bypasses entirely). For ordinary apps this is correct and you shouldn't change it; the threadpool concern only bites at high connection volume.
Querying specific record types

JS
import { resolve4, resolve6, resolveMx, resolveTxt, resolveCname } from 'node:dns/promises'

await resolve4('example.com')      // IPv4 A records → ['93.184.216.34']
await resolve6('example.com')      // IPv6 AAAA records
await resolveMx('example.com')     // [{ priority: 10, exchange: 'mail.example.com' }]
await resolveTxt('example.com')    // TXT records (SPF, verification tokens)
await resolveCname('www.example.com') // canonical name aliases

Record

Holds

Method

A / AAAA

IPv4 / IPv6 address

resolve4 / resolve6

MX

Mail servers (+ priority)

resolveMx

TXT

Arbitrary text (SPF, DKIM, verification)

resolveTxt

CNAME

Alias to another name

resolveCname

SRV

Service host + port

resolveSrv

NS

Authoritative name servers

resolveNs

Reverse DNS

JS
import { reverse } from 'node:dns/promises'

const hostnames = await reverse('93.184.216.34')   // IP → ['example.com', ...]
console.log(hostnames)
Reverse DNS (PTR) is often missing or untrustworthy
`reverse` looks up PTR records mapping an IP back to a name. Many IPs have no PTR record (you'll get `ENOTFOUND`), and the result is set by whoever controls the IP block — so it's **not** proof of identity. Use it for logging/diagnostics, never as a security check.
Controlling resolution

JS
import { setServers, getServers, Resolver } from 'node:dns/promises'

console.log(await getServers())          // current DNS servers
setServers(['8.8.8.8', '1.1.1.1'])       // override globally

// Or an isolated resolver with its own servers/timeout:
const resolver = new Resolver({ timeout: 2000, tries: 2 })
resolver.setServers(['1.1.1.1'])
console.log(await resolver.resolve4('example.com'))
Use a `Resolver` instance to avoid global state
`setServers` changes resolution for the **whole process**. When you need custom DNS servers for one part of your app (or to isolate tests), create a dedicated `Resolver` instance — it has its own servers, timeout, and retry settings and won't affect everything else. `resolve*` methods exist on the instance.
Common errors

Code

Meaning

ENOTFOUND

Hostname does not exist (NXDOMAIN) or no record

ESERVFAIL

DNS server failed to complete the query

ETIMEOUT

Query timed out (server unreachable/slow)

ENODATA

Name exists but has no record of the requested type

Practical guidance
  • For "I just need an IP to connect to," let the defaults work — http/net call lookup for you.

  • To inspect records (MX, TXT, SRV) or avoid the threadpool, use resolve*.

  • Under high connection volume, cache DNS results or prefer resolve* to keep the threadpool free.

  • Never trust reverse DNS for authentication.

Next
Up to the application layer — the module that powers web servers and clients: [The http Module](/nodejs/http-module).