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:
- Request Throughput (RPS): The number of complete request-response cycles executed per second.
- 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
- Warm-up phase: Execute several unrecorded requests before measuring to allow DNS resolution, TCP handshakes, and JIT compilation to settle.
- Control payload sizes: Use consistent response and request sizes when calculating data throughput to prevent skewed averages.
- Isolate client resources: Monitor local CPU and memory usage to ensure that the Node.js event loop does not become the bottleneck rather than the network.