Benchmark Network Throughput Using Axios

Benchmarking network throughput with the Axios HTTP client involves measuring the rate of data transferred over the network over a specific period through repeated requests. This article explains how to set up an accurate benchmark in Node.js using Axios, measure both download and upload throughput, manage concurrency and keep-alive connections, and calculate key performance metrics like Requests Per Second (RPS) and Megabytes Per Second (MB/s).

Key Metrics in Network Benchmarking

When testing network performance with repeated requests, two primary metrics define your throughput:

  1. Request Throughput (RPS): The number of complete request-response cycles executed per second.
  2. Data Throughput (MB/s or Mbps): The total volume of data (headers plus payload) transferred over the network divided by the total elapsed time.

Setting Up the Axios Benchmark Script

To obtain accurate measurements, configure Axios with a persistent HTTP agent using keepAlive: true. This prevents the overhead of creating a new TCP handshake for every single request, allowing you to measure steady-state network throughput.

import axios from 'axios';
import http from 'http';
import https from 'https';
import { performance } from 'perf_hooks';

// Configure persistent connections
const httpAgent = new http.Agent({ keepAlive: true, maxSockets: 50 });
const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 50 });

const client = axios.create({
  httpAgent,
  httpsAgent,
  responseType: 'arraybuffer' // Ensures accurate raw byte measurement
});

Implementing Repeated Requests and Measurement

The benchmarking function should execute a set number of requests across defined concurrency levels while recording byte counts and high-resolution timestamps.

async function runBenchmark({ url, totalRequests, concurrency }) {
  let completedRequests = 0;
  let totalBytesReceived = 0;
  let activeWorkers = 0;
  let requestIndex = 0;

  const startTime = performance.now();

  async function worker() {
    while (requestIndex < totalRequests) {
      requestIndex++;
      try {
        const response = await client.get(url);
        
        // Calculate size from headers or actual data length
        const contentLength = response.headers['content-length']
          ? parseInt(response.headers['content-length'], 10)
          : response.data.byteLength;

        totalBytesReceived += contentLength;
        completedRequests++;
      } catch (error) {
        console.error(`Request failed: ${error.message}`);
      }
    }
  }

  // Launch workers up to the concurrency limit
  const workers = Array.from({ length: concurrency }, () => worker());
  await Promise.all(workers);

  const endTime = performance.now();
  const durationInSeconds = (endTime - startTime) / 1000;

  return calculateResults(completedRequests, totalBytesReceived, durationInSeconds);
}

function calculateResults(requests, bytes, duration) {
  const megabytes = bytes / (1024 * 1024);
  const megabits = (bytes * 8) / (1000 * 1000);

  return {
    duration: `${duration.toFixed(2)} s`,
    completedRequests: requests,
    requestsPerSecond: (requests / duration).toFixed(2),
    throughputMBps: `${(megabytes / duration).toFixed(2)} MB/s`,
    throughputMbps: `${(megabits / duration).toFixed(2)} Mbps`
  };
}

// Example execution
(async () => {
  const results = await runBenchmark({
    url: 'https://httpbin.org/bytes/1048576', // 1 MB payload test endpoint
    totalRequests: 50,
    concurrency: 5
  });

  console.table(results);
})();

Benchmarking Upload Throughput

To benchmark upload throughput instead of download throughput, send fixed-size binary buffers via POST or PUT requests:

import crypto from 'crypto';

async function runUploadBenchmark({ url, totalRequests, concurrency, payloadSizeBytes }) {
  const payload = crypto.randomBytes(payloadSizeBytes);
  let totalBytesSent = 0;
  let completedRequests = 0;
  let requestIndex = 0;

  const startTime = performance.now();

  async function worker() {
    while (requestIndex < totalRequests) {
      requestIndex++;
      try {
        await client.post(url, payload, {
          headers: { 'Content-Type': 'application/octet-stream' }
        });
        totalBytesSent += payloadSizeBytes;
        completedRequests++;
      } catch (error) {
        console.error(`Upload failed: ${error.message}`);
      }
    }
  }

  const workers = Array.from({ length: concurrency }, () => worker());
  await Promise.all(workers);

  const durationInSeconds = (performance.now() - startTime) / 1000;
  return calculateResults(completedRequests, totalBytesSent, durationInSeconds);
}

Best Practices for Reliable Results