DEV Community

Cover image for Supercharge Your Axios Requests: axios-retryer for Robust API Communication
Serhii Zhabskyi
Serhii Zhabskyi

Posted on • Edited on

Supercharge Your Axios Requests: axios-retryer for Robust API Communication

I’ve been working on a React application that needed a robust solution for handling concurrent requests, custom retry logic, and token refresh—without shuffling together multiple scattered packages. I couldn’t find an all-in-one library that combined concurrency management, priorities, token refresh, caching, and circuit breaking in a straightforward API, so I built axios-retryer.

📝 Why I Wrote It

In this TV-based frontend project some data requests were absolutely critical (e.g., user authentication or top-level layout info) and needed to complete before all the nice-to-have background calls. Sure, I could let Axios blasts run wild, but that risked important calls getting stuck behind less urgent tasks. I also hated duplicating retry logic across different request modules. So, I decided to create a single “retryer” layer for concurrency, priority handling, advanced retry strategies, token refreshing, circuit-breaking, caching, and analytics events. Doing so helped me replace multiple incomplete solutions with a single, consistent approach.

🚀 Key Features at a Glance

  • Priority & Concurrency: Queue requests so critical calls finish before everything else, with fine-grained control over simultaneous requests.
  • Customizable Retry Strategies: Built-in linear, exponential backoff, static, or provide your own custom retry logic function.
  • TokenRefreshPlugin: Automatically refresh access tokens upon expiry and seamlessly replay pending requests.
  • CircuitBreakerPlugin: Intelligently detect repeated errors from an endpoint and temporarily halt requests to avoid further failures, then gracefully recover.
  • CachingPlugin: Cache responses for common requests to reduce load times and bandwidth usage.
  • Metrics & Events: Observe request outcomes, success/failure rates, circuit states, and other vital statistics through a rich event system.
  • Minimal Overhead: Approximately 6.4 kB minified + gzipped (core library with all plugins) means it won’t bloat your bundle.

⚙️ Quick Install & Hello World

Installation is straightforward using npm, yarn, or pnpm. Here’s the minimal setup to get you started:

# Using npm
npm install axios-retryer

# Using yarn
yarn add axios-retryer
Enter fullscreen mode Exit fullscreen mode
// Import the library
import { createRetryer } from 'axios-retryer';

async function main() {
  // Create a retryer instance with sensible defaults
  const retryer = createRetryer({
    retries: 3, // Attempt requests up to 3 times on failure
    debug: false // Set to true for development logging
  });

  // Use the axiosInstance provided by the retryer
  try {
    const response = await retryer.axiosInstance.get('https://api.example.com/data');
    console.log('Data:', response.data);
  } catch (error) {
    console.error('All retries failed:', error);
  }
}

main();
Enter fullscreen mode Exit fullscreen mode

That’s the essence: createRetryer() returns a manager object, and you use its axiosInstance property, which is a specially configured Axios instance with concurrency, retry logic, and plugin support baked in.

Priority & Concurrency

Priority and concurrency control were the main motivators for this library. In many applications, you can’t just fire off 20 requests simultaneously and hope they resolve in the right order. Instead, you often need to limit how many calls happen at once (maxConcurrentRequests), while also ensuring critical requests jump the queue (__priority).

  1. Concurrency: If you set maxConcurrentRequests to, say, 2, axios-retryer will ensure that no more than two requests are in-flight at any given time. This can mitigate server load and prevent the UI from becoming unresponsive under heavy network traffic.
  2. Priority: By passing a __priority option (numeric, typically using predefined constants) in your request config, you can dictate which requests get processed first from the queue. Lower numeric priority values in the constants (e.g., CRITICAL) mean higher actual priority.

Here’s a TypeScript snippet demonstrating this:

import { createRetryer, AXIOS_RETRYER_REQUEST_PRIORITIES } from 'axios-retryer';

const retryer = createRetryer({
  maxConcurrentRequests: 2, // Max two concurrent requests
  // Requests with priority >= HIGH will block lower priority ones
  blockingQueueThreshold: AXIOS_RETRYER_REQUEST_PRIORITIES.HIGH
});

async function exampleRequests() {
  // Critical request - gets processed sooner, potentially blocking others
  retryer.axiosInstance.get('/critical-endpoint', {
    __priority: AXIOS_RETRYER_REQUEST_PRIORITIES.CRITICAL
  }).then(response => console.log('Critical data received:', response.data));

  // Standard important request
  retryer.axiosInstance.get('/user-profile', {
    __priority: AXIOS_RETRYER_REQUEST_PRIORITIES.HIGH
  }).then(response => console.log('User profile received:', response.data));

  // Lower priority request - waits if concurrency is at capacity or blocked
  retryer.axiosInstance.get('/background-sync', {
    __priority: AXIOS_RETRYER_REQUEST_PRIORITIES.LOW
  }).then(response => console.log('Background sync complete:', response.data));
}

exampleRequests();
Enter fullscreen mode Exit fullscreen mode

Behind the scenes, axios-retryer manages an internal priority queue. When maxConcurrentRequests is reached, new requests are enqueued. Within that queue, items are sorted by their __priority. The blockingQueueThreshold adds another layer, ensuring that no requests with a priority lower than the threshold are processed if higher-priority requests are pending.

Retry Strategies (Linear, Exponential, Static, Custom)

We’ve all implemented the typical "retry a request 3 times if it fails," but sometimes a more sophisticated approach is needed. axios-retryer supports various backoff strategies:

  • Linear: Increments wait times consistently (e.g., 1s, 2s, 3s).
  • Exponential: Doubles the backoff period with each attempt (e.g., 1s, 2s, 4s, 8s). This is the default.
  • Static: Static backoff period (e.g. 2s, 2s, 2s, 2s).
  • Custom: Pass your own function to calculate delays.

You configure the retry behavior within the createRetryer options:

import { createRetryer, AXIOS_RETRYER_BACKOFF_TYPES } from 'axios-retryer';

const retryer = createRetryer({
  retries: 3, // Total retry attempts
  backoffType: AXIOS_RETRYER_BACKOFF_TYPES.EXPONENTIAL, // 'linear', 'exponential', 'static'
  // You can also specify retryable HTTP methods and status codes
  retryableMethods: ['get', 'head', 'options', 'put'],
  retryableStatuses: [408, 429, [500, 599]] // Retries on 408, 429, or 5xx
});

// This request will use the exponential backoff strategy defined above
retryer.axiosInstance.get('/some-flaky-endpoint')
  .catch(err => console.error('Failed after exponential backoff retries:', err));
Enter fullscreen mode Exit fullscreen mode

For a completely custom strategy, you can provide a retryStrategy object with isRetryable, shouldRetry, and getDelay functions:

import { createRetryer, createRetryStrategy } from 'axios-retryer';

const customStrategy = createRetryStrategy({
  isRetryable: (error) => !error.response || (error.response.status >= 500 && error.response.status < 600),
  shouldRetry: (error, attempt, maxRetries) => {
    if (error.config?.method?.toLowerCase() === 'post' && attempt >= 1) {
      return false; // Don't retry POST requests more than once
    }
    return attempt < maxRetries; // Note: attempt is 0-indexed for calculation
  },
  getDelay: (attempt) => (attempt + 1) * 1000 + (Math.random() * 500) // Linear backoff with jitter
});

const customRetryer = createRetryer({
  retries: 4,
  retryStrategy: customStrategy
});

customRetryer.axiosInstance.post('/another-endpoint', { data: 'payload' })
  .catch(err => console.error('Failed with custom strategy:', err));
Enter fullscreen mode Exit fullscreen mode

The library automatically handles the asynchronous timeouts for you.

TokenRefreshPlugin (Single Refresh + Replay)

Dealing with expired authentication tokens is a common pain point, especially if multiple requests are made around the token's expiration time. The TokenRefreshPlugin simplifies this by detecting token-related errors (typically 401 Unauthorized), automatically fetching a new token (once per expiration cycle), and then replaying the requests that failed due to the expired token.

Here’s how you set it up:

import { createRetryer } from 'axios-retryer';
import { createTokenRefreshPlugin } from 'axios-retryer/plugins/TokenRefreshPlugin';
import axios from 'axios'; // Import base axios for the refresh function if needed

const retryer = createRetryer({
  maxConcurrentRequests: 3
  // other configurations...
});

const tokenRefreshPlugin = createTokenRefreshPlugin(
  async (axiosInstanceForRefresh) => {
    // This function is called to get a new token
    // It receives an Axios instance specifically for the refresh call
    // to avoid circular dependencies with the retryer's interceptors.
    console.log('Attempting to refresh token...');
    const refreshToken = localStorage.getItem('refreshToken');
    // Use a clean axios instance for the refresh call
    const { data } = await axios.post('/auth/refresh', { refreshToken });
    localStorage.setItem('accessToken', data.accessToken);
    console.log('Token refreshed successfully.');
    return { token: data.accessToken }; // Must return the new token
  },
  {
    // Optional configuration for the plugin
    authHeaderName: 'Authorization',    // Default: 'Authorization'
    tokenPrefix: 'Bearer ',             // Default: 'Bearer '
    refreshStatusCodes: [401],          // Default: [401]
    maxRefreshAttempts: 2,              // Default: 3
    // Custom detector for APIs that don't use 401 (e.g., GraphQL)
    customErrorDetector: (response) => {
      return response?.data?.errors?.some(
        (err: any) => err.extensions?.code === 'UNAUTHENTICATED' || err.message?.includes('token expired')
      );
    }
  }
);

// Add the plugin to the retryer instance
retryer.use(tokenRefreshPlugin);

// Now, requests made with retryer.axiosInstance will benefit from auto token refresh
retryer.axiosInstance.get('/secure-data')
  .then(response => console.log('Secure data:', response.data))
  .catch(error => console.error('Failed to get secure data:', error));
Enter fullscreen mode Exit fullscreen mode

When a request returns a status code matching refreshStatusCodes (or if customErrorDetector returns true), the plugin pauses outgoing requests, executes your fetchNewToken function, updates the default headers of the retryer.axiosInstance with the new token, and then replays the original failed request and any other requests that were queued while the token was being refreshed.

CircuitBreakerPlugin

To prevent an application from repeatedly trying to connect to a service that is known to be failing, you can use the CircuitBreakerPlugin. Once a configured error threshold is met (e.g., 5 failed requests in a row), the circuit "opens," and subsequent requests to that service fail immediately (short-circuit) without hitting the network. After a cooldown period, the circuit transitions to a "half-open" state, allowing a limited number of test requests. If these succeed, the circuit "closes" and normal operation resumes. If they fail, it re-opens.

import { createRetryer } from 'axios-retryer';
import { createCircuitBreaker } from 'axios-retryer/plugins/CircuitBreakerPlugin'; // Corrected import name

const retryer = createRetryer({
  // other configurations...
});

const circuitBreakerPlugin = createCircuitBreaker({
  failureThreshold: 5,     // Trip after 5 consecutive failures
  openTimeout: 30000,      // Stay open for 30 seconds
  halfOpenMax: 2,          // Allow 2 test requests in half-open state
  successThreshold: 2,     // Need 2 successes in half-open to close
  useSlidingWindow: false  // Set to true for time-based failure counting
});

retryer.use(circuitBreakerPlugin);

// Requests to a consistently failing service will eventually be short-circuited
function fetchDataRepeatedly() {
  retryer.axiosInstance.get('/unreliable-service')
    .then(response => console.log('Service responded:', response.data))
    .catch(error => {
      console.error('Service call failed:', error.message);
      if (error.isAxiosError && error.message.includes('Circuit is open')) {
        console.log('Circuit breaker is open, not attempting network request.');
      }
    });
}

// Simulate multiple calls
// setInterval(fetchDataRepeatedly, 2000);
Enter fullscreen mode Exit fullscreen mode

This pattern is crucial for building resilient applications that can gracefully handle temporary outages of downstream services.

CachingPlugin

The CachingPlugin (using createCachePlugin) helps you cache responses from specific endpoints, reducing unnecessary network calls and improving perceived performance. You can configure cache lifetime, which HTTP methods to cache, and more.

import { createRetryer } from 'axios-retryer';
import { createCachePlugin } from 'axios-retryer/plugins/CachingPlugin'; // Corrected import name

const retryer = createRetryer({
  // other configurations...
});

const cachePlugin = createCachePlugin({
  timeToRevalidate: 60 * 1000, // Cache entries for 1 minute (in ms)
  cacheMethods: ['GET'],       // Only cache GET requests (default)
  cleanupInterval: 5 * 60 * 1000, // Check for stale entries every 5 minutes
  maxItems: 100,               // Store up to 100 cached items
  compareHeaders: false,       // Set to true to include headers in cache key
  cacheOnlyRetriedRequests: false // Set to true to only cache successful retried requests
});

retryer.use(cachePlugin);

async function fetchCachedData() {
  console.log('Fetching /static-data...');
  const response1 = await retryer.axiosInstance.get('/static-data');
  console.log('Data 1:', response1.data, '(from network or cache)');

  console.log('Fetching /static-data again...');
  const response2 = await retryer.axiosInstance.get('/static-data');
  console.log('Data 2:', response2.data, '(likely from cache if within TTR)');
}

fetchCachedData();

// You can also control caching on a per-request basis
retryer.axiosInstance.get('/api/realtime-updates', {
  __cachingOptions: {
    cache: false // Disable caching for this specific request
  }
});

retryer.axiosInstance.post('/api/submit-form', { data: 'content' }, {
  __cachingOptions: {
    cache: true, // Force caching for this POST request (if method allowed globally)
    ttr: 30000   // Custom Time To Revalidate: 30 seconds
  }
});
Enter fullscreen mode Exit fullscreen mode

The plugin automatically generates cache keys based on the request URL, method, and optionally headers. It provides methods to invalidate or clear the cache programmatically.

Metrics/Events

Monitoring real-time performance and understanding request behavior can help you catch bottlenecks or diagnose issues. axios-retryer emits a variety of events throughout the request lifecycle, allowing you to wire up custom analytics or logging.

You subscribe to these events using the on method of the retryer instance:

import { createRetryer } from 'axios-retryer';

const retryer = createRetryer({ debug: true }); // Enable debug for more event logs

retryer
  .on('onRetryProcessStarted', (requestConfig) => {
    console.log(`[Event] Retry process started for: ${requestConfig.url}`);
  })
  .on('beforeRetry', (requestConfig, attemptInfo) => {
    console.log(`[Event] Before retry attempt ${attemptInfo.attemptNumber} for: ${requestConfig.url}`);
  })
  .on('afterRetry', (requestConfig, attemptInfo, wasSuccessful) => {
    console.log(`[Event] After retry attempt ${attemptInfo.attemptNumber} for: ${requestConfig.url}. Success: ${wasSuccessful}`);
  })
  .on('onRetryProcessFinished', (requestConfig, metrics) => {
    console.log(`[Event] Retry process finished for: ${requestConfig.url}. Metrics:`, metrics);
    // metrics include totalAttempts, wasSuccessful, etc.
  })
  .on('onMetricsUpdated', (globalMetrics) => {
    // These are overall metrics for the retryer instance
    // console.log('[Event] Global metrics updated:', globalMetrics);
    // updateDashboard(globalMetrics);
  })
  .on('onTokenRefreshed', (newToken) => {
    console.log('[Event] Token was successfully refreshed.');
  });

// Example request to trigger events
retryer.axiosInstance.get('https://api.example.com/flaky')
  .catch(() => console.log('Request to flaky endpoint eventually failed or succeeded.'));
Enter fullscreen mode Exit fullscreen mode

The globalMetrics object provided by onMetricsUpdated includes counts for total requests, successful retries, failed retries, and new timer health statistics to help monitor event loop congestion.

💡 Less-Obvious Use Cases

Beyond the standard retry or concurrency scenarios, here are some other ways axios-retryer can be beneficial:

  1. Offline-First PWAs: In Progressive Web Apps, use mode: 'manual' to store failed requests when offline. When the connection is re-established, call retryer.retryFailedRequests() to sync critical data, respecting priorities.
  2. Serverless Edge Functions: When calling external APIs from environments with strict concurrency limits (like some edge function providers), maxConcurrentRequests helps stay within quotas.
  3. React Query/SWR Adapter: While libraries like React Query or SWR handle their own caching and some retries, axios-retryer can manage the underlying Axios instance to provide global concurrency limits, priority queuing, and standardized token refresh logic for all data fetches.
  4. Robust Microservice Communication (Node.js): If your Node.js backend acts as an API gateway or communicates with multiple microservices, axios-retryer (with plugins like Circuit Breaker) can make these interactions more resilient.
  5. Batch Data Uploads/Processing: For scenarios where you need to upload many items or process a batch of API calls, concurrency control ensures you don't overwhelm the server, and retries handle intermittent failures gracefully.
  6. Web Scraping Responsibly: When scraping data, use maxConcurrentRequests and appropriate backoff strategies with queueDelay to avoid overwhelming the target server and to respect its rate limits.

📈 Performance & Bundle Size

axios-retryer is designed to be efficient. The core library, including all plugins, comes in at approximately 6.4 kB minified and gzipped. The plugin system is modular, so if you don't use a plugin, its code won't be included in your bundle when using tree-shaking.

Performance considerations:

  • The priority queue uses efficient data structures (like a binary heap) for O(log n) operations.
  • Retry strategies add minimal overhead, primarily driven by JavaScript timers.
  • The new timer management system actively monitors and helps prevent event loop congestion from excessive timers.

It should integrate smoothly into typical front-end or Node.js projects without significantly impacting bundle size or runtime performance.

🛣️ Roadmap & Contribution Call

I have several features and improvements planned for future releases:

  • WebSocket Reconnection Plugin: A dedicated plugin to manage robust WebSocket connections, including automatic reconnection with backoff strategies.
  • DevTools Panel: A browser extension (e.g., for Chrome/Firefox DevTools) to visualize the request queue, circuit breaker states, cache contents, and active timers.
  • More Granular Cache Control: Advanced cache invalidation strategies and tagging.

Contributions are highly welcome! Whether it's tackling one of these features, suggesting new ones, improving documentation, or reporting bugs, your input is valuable. Please check out the CONTRIBUTING.md file on GitHub, open an issue to discuss your ideas, or submit a pull request.

🏁 Conclusion / CTA

If you're tired of cobbling together solutions for API request concurrency, complex retries, token refreshing, circuit breaking, and caching, axios-retryer offers a unified and powerful approach.

Install: npm install axios-retryer

Explore the documentation, examples, and source code on GitHub. If you find it useful, please consider giving it a star! I’d love to hear your feedback, learn about your use cases, and see how axios-retryer can be improved to better serve the developer community.

Top comments (0)