How to Propagate X-Request-ID Header Using Axios

Propagating tracing identifiers like X-Request-ID across microservices is essential for maintaining observability and debugging distributed systems. This guide explains how to capture an incoming request ID in a Node.js environment, preserve it throughout the execution lifecycle using AsyncLocalStorage, and automatically forward it in all outgoing HTTP requests using Axios interceptors.

1. Store Request Context with AsyncLocalStorage

In asynchronous Node.js applications, passing a request ID manually through every function call creates tight coupling and code bloat. The cleanest approach is using Node.js's built-in AsyncLocalStorage from the async_hooks module to store the tracing context per request execution chain.

// context.js
const { AsyncLocalStorage } = require('async_hooks');

const requestContext = new AsyncLocalStorage();

module.exports = { requestContext };

2. Capture the Header in an Express Middleware

Extract the incoming X-Request-ID header from the incoming request or generate a new one using crypto.randomUUID() if the header is absent. Wrap the execution of downstream middleware and route handlers within the requestContext.run() method.

// middleware.js
const crypto = require('crypto');
const { requestContext } = require('./context');

function correlationIdMiddleware(req, res, next) {
  const requestId = req.headers['x-request-id'] || crypto.randomUUID();
  
  // Attach to response headers for client tracking
  res.setHeader('X-Request-ID', requestId);

  // Store in context for outgoing requests
  requestContext.run(new Map([['requestId', requestId]]), () => {
    next();
  });
}

module.exports = { correlationIdMiddleware };

3. Attach the Header Using Axios Request Interceptors

Create a dedicated Axios instance and register a request interceptor. The interceptor retrieves the current requestId from AsyncLocalStorage before the HTTP request is dispatched and injects it into the request headers.

// httpClient.js
const axios = require('axios');
const { requestContext } = require('./context');

const apiClient = axios.create({
  timeout: 5000,
});

apiClient.interceptors.request.use((config) => {
  const store = requestContext.getStore();
  
  if (store && store.has('requestId')) {
    config.headers['X-Request-ID'] = store.get('requestId');
  }

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

module.exports = { apiClient };

4. Direct Injection Without AsyncLocalStorage

If you are not using asynchronous context tracking, you can inject the header explicitly per request by passing headers into individual Axios calls or creating a short-lived instance:

// Explicit header passing
async function callDownstreamService(requestId, payload) {
  return axios.post('https://api.example.com/data', payload, {
    headers: {
      'X-Request-ID': requestId,
    },
  });
}

Using Axios request interceptors combined with AsyncLocalStorage ensures seamless, non-intrusive header propagation throughout your entire service architecture without modifying business logic.