Axios Exponential Backoff with Full Jitter

Implementing exponential backoff with full jitter in Axios improves application resilience and prevents the "thundering herd" problem by randomizing and spacing out retry intervals for failed requests. This guide demonstrates how to configure Axios interceptors to detect transient network and server errors, calculate exponential delays with full jitter, and automatically re-execute failed HTTP requests safely.

The Mathematics of Full Jitter

Standard exponential backoff multiplies the base delay by \(2^n\) (where \(n\) is the retry attempt). However, if many clients fail at the same time, they will all retry at identical intervals. "Full Jitter" mitigates this by selecting a uniform random value between zero and the calculated exponential backoff ceiling:

\[\text{Delay}_{\text{max}} = \min(\text{MaxDelay}, \text{BaseDelay} \times 2^{\text{retryCount}})\] \[\text{SleepTime} = \text{random}(0, \text{Delay}_{\text{max}})\]

Implementation with Axios Interceptors

Axios interceptors allow you to intercept HTTP responses and retry the original request configuration before returning an error to the caller.

import axios from 'axios';

// 1. Define backoff settings
const RETRY_CONFIG = {
  maxRetries: 4,
  baseDelayMs: 1000,
  maxDelayMs: 16000,
  retryableStatuses: [408, 429, 500, 502, 503, 504],
};

// 2. Full Jitter Calculation
function calculateFullJitter(attempt, baseDelay, maxDelay) {
  const exponentialBackoff = Math.min(maxDelay, baseDelay * Math.pow(2, attempt));
  // Random delay between 0 and exponentialBackoff
  return Math.floor(Math.random() * exponentialBackoff);
}

// 3. Create the Axios instance
const apiClient = axios.create({
  timeout: 10000,
});

// 4. Attach the response interceptor
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const { config, response } = error;

    // Do not retry if request config is missing
    if (!config) {
      return Promise.reject(error);
    }

    // Initialize the retry count
    config.__retryCount = config.__retryCount || 0;

    // Determine if the error is transient and retryable
    const isNetworkError = !response && error.code !== 'ECONNABORTED';
    const isRetryableStatus = response && RETRY_CONFIG.retryableStatuses.includes(response.status);
    const canRetry = config.__retryCount < RETRY_CONFIG.maxRetries;

    if ((isNetworkError || isRetryableStatus) && canRetry) {
      config.__retryCount += 1;

      const delay = calculateFullJitter(
        config.__retryCount,
        RETRY_CONFIG.baseDelayMs,
        RETRY_CONFIG.maxDelayMs
      );

      // Wait for the calculated jitter delay
      await new Promise((resolve) => setTimeout(resolve, delay));

      // Retry the request
      return apiClient(config);
    }

    return Promise.reject(error);
  }
);

export default apiClient;

Key Considerations