npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mittwald/axios-cache-with-retry

v1.0.0

Published

Coordinated retry, cache and in-flight dedupe for Axios, as a single adapter

Readme

@mittwald/axios-cache-with-retry

Coordinated retry, cache and in-flight dedupe for Axios — as a single adapter rather than three interceptors that each re-run the request behind each other's back.

Retry and caching cannot be composed out of separate interceptors: a retry is a second dispatch, so it re-enters the interceptor chain and every layer around it sees a request the caller never made. Combining axios-retry with axios-cache-interceptor therefore multiplies requests instead of deduplicating them, and which of the two even gets to see a failure depends on validateStatus. Why this exists walks through a concrete example.

Installation

npm install @mittwald/axios-cache-with-retry

axios is a peer dependency (^1).

Usage

import axios from "axios";
import { setupAxiosRetryCache } from "@mittwald/axios-cache-with-retry";

const client = setupAxiosRetryCache(axios.create(), {
  requestKey: ({ config }) => `${config.method ?? "get"}:${config.url}`,
  cache: { ttl: 60_000, staleIfError: true },
  retry: { retries: 3, retryOnStatus: [408, 429, 500, 502, 503, 504] },
});

await client.get("/users");
await client.retryCache.invalidate("get:/users");

The retry decision is made for thrown Axios errors and for regular responses alike, so it keeps working under validateStatus: () => true.

setupAxiosRetryCache replaces the instance's adapter and returns the same instance, typed with an added retryCache property. Calling it twice on one instance re-wraps the original adapter rather than stacking two layers.

Per-request options

Every request config accepts a retryCache field that overrides the global options for that one request. The module augments Axios' own AxiosRequestConfig, so the field is typed wherever axios is.

await client.get("/users", { retryCache: { retry: { retries: 5 } } });
await client.get("/metrics", { retryCache: { cache: false } });
await client.get("/raw", { retryCache: false });

| Value | Effect | | ------------------- | ------------------------------------------------ | | false | Bypasses the adapter entirely | | true | Enables cache and retry with the global settings | | { cache, retry } | Merges into the global settings for this request | | { dedupe: false } | Opts this request out of in-flight deduplication |

Options

cache

| Option | Default | Description | | -------------- | ---------------- | ------------------------------------------------------------ | | enabled | true | Set to false to keep the cache off until a request opts in | | ttl | 60000 | Lifetime of an entry in milliseconds | | methods | ["get","head"] | Methods whose responses are cached | | staleIfError | false | Serve an expired entry once retries are exhausted | | shouldCache | — | Predicate deciding whether a response is stored |

retry

| Option | Default | Description | | --------------------- | ----------------------------------- | -------------------------------------------------------- | | enabled | true | Set to false to keep retry off until a request opts in | | retries | 2 | Attempts after the initial one | | methods | ["get","head","options"] | Methods that may be retried | | retryOnStatus | 408, 425, 429, 500, 502, 503, 504 | Status codes that trigger a retry | | retryOnNetworkError | true | Retry errors that never produced a response | | respectRetryAfter | true | Honour a Retry-After response header | | delay | exponential, capped at 30 s | Fixed milliseconds or a function | | shouldRetry | — | Replaces the built-in decision entirely |

requestKey

Cache and dedupe key for a request. Defaults to method, base URL, URL, a stably serialized params and — for non-GET/HEAD — a stably serialized body. Returning undefined excludes the request from both caching and deduplication.

storage

Defaults to an in-memory store. Pass createMemoryStorage({ maxEntries }) for a bounded one, or any object implementing RetryCacheStorage; every method may return a promise, so an async backend works as well.

Cache API

client.retryCache exposes get, set, invalidate, invalidatePrefix and clear. invalidatePrefix needs a storage that implements either deletePrefix or keys — the built-in memory storage implements both.

Behaviour worth knowing

  • Deduplication wraps the whole operation, retries included: concurrent callers with the same key wait for one shared result and each receive their own shallow copy.
  • A cache hit short-circuits before retry, so a cached response never produces a request.
  • staleIfError only serves an expired entry after retries are exhausted, never instead of a retry.
  • Only responses are cached, never errors.

Why this exists

Retry, cache and in-flight deduplication are three decisions about the same request, but an interceptor can only see a request on its way out and a response on its way back. It cannot wrap the dispatch itself — and a retry is by definition a second dispatch. So an interceptor-based retry has to re-send the config, which means re-entering the interceptor chain, which means every other layer sees a request the caller never made. Stacking the layers is the only composition the interceptor API offers, and both stacking orders are wrong.

The popular packages each solve one third of the problem and sit in a position that collides with the others:

  • axios-retry and retry-axios hook the response (error) path and retry by re-dispatching the request config.
  • axios-cache-interceptor caches and deduplicates concurrent requests from a request/response interceptor pair, with its own in-flight bookkeeping keyed per request.
  • axios-cache-adapter (unmaintained) and axios-extensions (cacheAdapterEnhancer, throttleAdapterEnhancer) take the adapter position instead — so they cannot coexist with each other, or with anything else that wants to own the adapter.

Where it snags

Three components ask for GET /users at the same time. The endpoint answers 503 twice and then 200. Retry is configured for two extra attempts, and the cache layer deduplicates in-flight requests. One logical request, one expected outcome: three responses out of three network calls.

Deduplication inside the retry (cache layer closest to the request): dedupe collapses the three callers onto one dispatch, that dispatch fails with 503, and the rejection fans back out to all three callers — each of which then runs its own retry handler, because each call walks the interceptor chain separately. Three retry loops instead of one, up to nine network calls for one logical GET, and whichever loop happens to finish first decides what ends up in the cache while the others are still retrying.

Retry inside the deduplication (retry layer closest to the request): every re-dispatch looks like a brand-new request to the cache layer above it, while that layer's in-flight entry for the first attempt is still open and still waiting to be settled by an attempt that has already been abandoned. Depending on how the cache keys its pending state, the second attempt either registers alongside the first — so the entry that gets stored is not the response the caller received — or waits on a promise the retry loop is never going to fulfil.

And the two layers disagree about what a failure is. Retry lives on the error path, the cache lives on the success path, and which one a 503 takes is the caller's validateStatus setting. Accept non-2xx responses and the retry handler is never invoked at all, while the cache stores the 503 and serves it for the rest of its TTL.

What this package does instead

All three concerns live in a single adapter, the one place that does wrap the dispatch. One request key is resolved once, and one shared in-flight promise covers the entire operation: the cache lookup, the retry loop, and the cache write after the final attempt. Concurrent callers wait for that one result instead of starting their own loops, retries never re-enter the interceptor chain, and the retry decision is made for returned responses and thrown errors alike — so validateStatus stops being part of the retry semantics. The guarantees that follow from it are listed under Behaviour worth knowing above.

License

MIT