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.