Monitor HTTP Payload Size with Axios Interceptors

Monitoring the size of HTTP requests and responses at runtime is essential for optimizing network performance, diagnosing bandwidth bottlenecks, and enforcing payload limits. This guide demonstrates how to calculate and log the payload sizes of outgoing requests and incoming responses by leveraging Axios interceptors in both browser and Node.js environments.


Why Use Interceptors for Payload Monitoring?

Axios interceptors allow you to hook into the HTTP lifecycle before a request is sent and before a response reaches your application logic. Using interceptors for payload monitoring provides a centralized, non-intrusive way to measure network traffic across all API calls without modifying individual endpoint handlers.


Calculating Payload Size

Because payloads can be strings, JSON objects, FormData, or binary buffers, size calculations depend on the data type and the runtime environment:


Step-by-Step Implementation

1. Create a Helper Function to Measure Size

Create a utility function that safely computes the byte size of various payload types across runtimes:

function getPayloadSize(data) {
  if (!data) return 0;

  // If already a Buffer (Node.js)
  if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) {
    return data.length;
  }

  // If a string
  if (typeof data === 'string') {
    return typeof Buffer !== 'undefined'
      ? Buffer.byteLength(data, 'utf8')
      : new Blob([data]).size;
  }

  // If a plain JavaScript object
  if (typeof data === 'object') {
    try {
      const serialized = JSON.stringify(data);
      return typeof Buffer !== 'undefined'
        ? Buffer.byteLength(serialized, 'utf8')
        : new Blob([serialized]).size;
    } catch {
      return 0;
    }
  }

  return 0;
}

2. Configure the Request Interceptor

The request interceptor intercepts outgoing configuration, calculates the request body size, and records metadata (like timestamps) onto the configuration object:

import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
});

apiClient.interceptors.request.use(
  (config) => {
    const requestSize = getPayloadSize(config.data);
    config.metadata = {
      startTime: Date.now(),
      requestSizeInBytes: requestSize,
    };

    console.log(`[Request] ${config.method?.toUpperCase()} ${config.url} - Payload Size: ${requestSize} bytes`);

    return config;
  },
  (error) => Promise.reject(error)
);

3. Configure the Response Interceptor

The response interceptor measures the incoming response payload. It first attempts to read the standard Content-Length header. If the header is missing (such as when Transfer-Encoding: chunked is used), it calculates the size from the response body directly:

apiClient.interceptors.response.use(
  (response) => {
    const contentLength = response.headers['content-length'];
    const responseSize = contentLength
      ? parseInt(contentLength, 10)
      : getPayloadSize(response.data);

    const duration = Date.now() - (response.config.metadata?.startTime || Date.now());
    const requestSize = response.config.metadata?.requestSizeInBytes || 0;

    console.log(
      `[Response] ${response.status} ${response.config.url} ` +
      `(${duration}ms) - Request: ${requestSize} B, Response: ${responseSize} B`
    );

    return response;
  },
  (error) => {
    if (error.response) {
      const errorSize = getPayloadSize(error.response.data);
      console.warn(
        `[Error Response] ${error.response.status} ${error.config?.url} - Error Payload: ${errorSize} bytes`
      );
    }
    return Promise.reject(error);
  }
);

export default apiClient;

Best Practices