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
// 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();
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
).
- 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. - 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();
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));
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));
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));
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);
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
}
});
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.'));
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:
- Offline-First PWAs: In Progressive Web Apps, use
mode: 'manual'
to store failed requests when offline. When the connection is re-established, callretryer.retryFailedRequests()
to sync critical data, respecting priorities. - Serverless Edge Functions: When calling external APIs from environments with strict concurrency limits (like some edge function providers),
maxConcurrentRequests
helps stay within quotas. - 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. - 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. - 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.
- Web Scraping Responsibly: When scraping data, use
maxConcurrentRequests
and appropriate backoff strategies withqueueDelay
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)