Capture DNS Lookup Timings in Axios

Capturing detailed DNS lookup timings in Axios requires tapping into Node.js's underlying network socket events. While Axios does not provide built-in DNS metrics out of the box, you can measure the precise duration of the domain resolution phase by attaching listeners to the socket and its lookup event via Axios interceptors or custom HTTP agents.

Understanding the Network Socket Lifecycle

In Node.js, an outgoing HTTP request follows a distinct connection lifecycle:

  1. Socket initialization: The request is assigned a socket.
  2. DNS Lookup (lookup): The domain name is resolved to an IP address.
  3. TCP Connection (connect / secureConnect): The TCP (and optional TLS) handshake is established.
  4. Data transfer: The HTTP payload is sent and the response is received.

The DNS lookup duration is the time elapsed between when the socket begins domain resolution and when the lookup event fires.

Implementing DNS Timing with Axios Interceptors

The most straightforward way to capture this metric is by using Axios request and response interceptors to attach event listeners to the underlying http.ClientRequest instance.

const axios = require('axios');

const axiosInstance = axios.create();

axiosInstance.interceptors.request.use((config) => {
  config.metadata = { startTime: process.hrtime.bigint() };

  return config;
});

axiosInstance.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// Function to make a request and track DNS timings
async function makeTimedRequest(url) {
  const timings = {
    dnsLookup: null,
    totalTime: null,
  };

  let dnsStart;

  const response = await axiosInstance.get(url, {
    transport: {
      request: (options, callback) => {
        const http = options.protocol === 'https:' ? require('https') : require('http');
        const req = http.request(options, callback);

        req.on('socket', (socket) => {
          dnsStart = process.hrtime.bigint();

          socket.on('lookup', (err, address, family, host) => {
            const dnsEnd = process.hrtime.bigint();
            // Convert nanoseconds to milliseconds
            timings.dnsLookup = Number(dnsEnd - dnsStart) / 1e6;
          });
        });

        return req;
      },
    },
  });

  return { data: response.data, timings };
}

Handling Connection Reuse and Keep-Alive

When keepAlive is enabled in http.Agent or https.Agent, DNS resolution is skipped for pooled, reused connections. In such cases, the lookup event will not fire.

To account for connection pooling:

const http = require('http');
const https = require('https');

const httpAgent = new http.Agent({ keepAlive: true });
const httpsAgent = new https.Agent({ keepAlive: true });

const client = axios.create({
  httpAgent,
  httpsAgent,
});

If timings.dnsLookup remains undefined or unset during a request, it confirms that a persistent socket was reused and no DNS query was dispatched.