mrkeyoor.com_
Sat 19 Sept 23:48 UTC
npmUtilsupdated 19 Sept 2026

lru-cache review

lru-cache 11.5.2 is an in-process JavaScript cache that evicts the least recently used entries when a count or calculated-size bound is reached. It supports per-entry TTLs, stale reads, disposal hooks, object keys, async loading through `fetchMethod`, request coalescing, status objects, and Node diagnostics channels. Version 11.5 adds `backgroundFetchSize`, which assigns a provisional size to an in-flight background fetch when no stale value is present. That closes a capacity-accounting gap for size-bounded caches while work is still pending.

509.6Mdownloads / wk
Verdict

lru-cache 11.5.2 installed in 0.5 seconds as 1 dependency-free package, used 3 MB on our box, and returned 0 audit findings. Install it for a bounded per-process cache with coalesced async loads; choose a shared store or TTL-first design when process boundaries or exact expiry drive the requirement.

We installed it

Lab card: what happened when we installed lru-cacheScreenshot of lru-cache documentation
Install✓ · 0.5s1 package on disk · 3 MB
ImportESM import works · require() works · ESM package with exports map
Browser5.8 KBgzipped (18.3 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does lru-cache install cleanly?

Yes. In a fresh container with an empty cache, npm install lru-cache finished in 0.5s, leaving 1 package and 3 MB on disk. npm audit reported no known vulnerabilities.

How much does lru-cache add to a browser bundle?

5.8 KB gzipped (18.3 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does lru-cache work with both ESM and CommonJS?

Yes. Both import 'lru-cache' and require('lru-cache') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does lru-cache include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

lru-cache or quick-lru: which should you use?

quick-lru: Use it for a smaller count-bounded cache when async fetches, size accounting, and diagnostics are unnecessary. lru-cache 11.5.2 installed in 0.5 seconds as 1 dependency-free package, used 3 MB on our box, and returned 0 audit findings.

When should you not use lru-cache?

Cached state must be shared across processes, hosts, or deploys. lru-cache keeps values only inside the current JavaScript process.

API stability3/5The `LRUCache` class and its map-like `get`, `set`, `has`, `delete`, and `clear` methods are steady within version 11, but recent majors changed exports, Node support, type locations, fetch context, null handling, and async return types. Version 11 itself drops Node below 20. Pinning the major is sensible for libraries that expose cache options or TypeScript types in their own public API.
Docs5/5The README explains storage bounds, TTL deletion, undefined values, fetch controls, object-key identity, observability overhead, platform-specific diagnostics, timer testing, and performance tradeoffs. The linked TypeDoc site covers constructor options and status types in detail. Examples name unsafe combinations instead of presenting every option as equally advisable, which is especially useful for memory-sensitive code.
Maintenance5/5GitHub reports 5,909 stars, 3 combined open issues and pull requests, an unarchived repository, and a push on 7 July 2026. npm published 11.5.2 that day. The current changelog records feature work across 11.1 through 11.5 plus targeted fetch fixes in 11.4. The small open queue and same-day source and package activity indicate close maintenance of the released line.
Ecosystem5/5npm recorded 554,574,259 downloads for the latest completed week, placing this package deep in the Node dependency graph. Version 11 supplies ESM and CommonJS conditions, browser-specific exports, TypeScript declarations, map-like methods, and Node diagnostics integration. That reach does not make entries portable: every cache still belongs to one runtime process and disappears on restart.

Discussed on

  1. hnSparse File LRU Cache52 points
  2. hnRedis as an LRU cache (and the mystery of port 6379)22 points
  3. hnNginx + redis + lua reverse-proxy LRU cache10 points
  4. hnA LRU cache implementation8 points
  5. hnImplementing an Efficient LRU Cache in JavaScript6 points

Use it if

  • One Node process needs a bounded hot-data cache with predictable least-recently-used eviction.
  • Concurrent misses for the same key should share one `fetchMethod` promise and optionally serve a stale value during refresh.
  • Entries vary in cost, so `maxSize`, `sizeCalculation`, and `maxEntrySize` need to enforce a budget other than item count.
  • Cache hits, misses, evictions, TTL state, and fetch timing must feed status objects or `node:diagnostics_channel`.
Skip it if

Setup reality

We installed lru-cache 11.5.2 in a fresh Node 22 Bookworm sandbox in 0.5 seconds. It left 1 package and 3 MB on disk, with 0 direct dependencies and 0 peer dependencies. npm audit found 0 known vulnerabilities. Both require() and ESM import worked, and TypeScript declarations are bundled. Our browser build measured 18.3 KB minified and 5.8 KB gzipped. The package license is BlueOak-1.0.0 and its engine range is Node 20 or 22 and newer.

Construction must include max, maxSize, or ttl; otherwise storage has no useful boundary. A positive max preallocates item slots and is the straightforward fast path. maxSize requires every stored item to provide a positive integer size, either through sizeCalculation or the individual set. A TTL-only cache does not proactively delete stale items unless ttlAutopurge is enabled, so untouched entries can retain memory after expiry.

fetchMethod turns cache.fetch(key) into a coalesced async loader. Calls for the same missing key share the in-flight request. Options decide whether stale data is returned during refresh, after rejection, or after abort. Version 11.5's backgroundFetchSize gives a pending fetch a default effective size of 1 when it does not shadow a stale value. Set it deliberately when maxSize units represent bytes or another cost scale.

Keys use identity: two separately created {id: 1} objects are different keys. undefined cannot be stored. Fake-timer tests also need care because the module captures performance or Date at import time; dynamically import after enabling timers or pass a perf object with now(). Browser and edge builds can miss the first tick of diagnostics setup because the fallback loads node:diagnostics_channel dynamically.

Patterns

Bound entries by count create-count-cache

import { LRUCache } from 'lru-cache';

const cache = new LRUCache({ max: 500 });
cache.set('user:42', profile);
const hit = cache.get('user:42');

A positive `max` preallocates room for 500 entries and avoids the unbounded-storage warning.

Account for different value sizes bound-by-size

import { LRUCache } from 'lru-cache';

const cache = new LRUCache({
  maxSize: 10 * 1024 * 1024,
  maxEntrySize: 512 * 1024,
  sizeCalculation: (value) => Buffer.byteLength(value),
});

Every item needs a positive integer size when `maxSize` is active; this example rejects entries above 512 KB.

Add lazy TTL expiry expire-items

import { LRUCache } from 'lru-cache';

const cache = new LRUCache({
  max: 1_000,
  ttl: 60_000,
});
cache.set('session:7', session, { ttl: 5_000 });

A 5-second entry is treated as missing after expiry, but it is normally removed only when accessed or evicted.

Delete expired entries on timers autopurge-expired

import { LRUCache } from 'lru-cache';

const cache = new LRUCache({
  ttl: 30_000,
  ttlAutopurge: true,
});

`ttlAutopurge` adds timer work; use it only when stale, untouched entries must leave memory near the 30-second deadline.

Coalesce concurrent cache misses fetch-on-miss

import { LRUCache } from 'lru-cache';

const cache = new LRUCache({
  max: 500,
  fetchMethod: async (key, staleValue, { signal }) => {
    const response = await fetch(`/api/items/${key}`, { signal });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  },
});
const item = await cache.fetch('42');

Concurrent `fetch('42')` calls share 1 in-flight loader unless options force a refresh.

Return stale data during refresh serve-stale-refresh

const value = await cache.fetch('catalog', {
  allowStale: true,
  forceRefresh: true,
  noDeleteOnFetchRejection: true,
});

This can return the stale value immediately while 1 background request refreshes it; failed refreshes keep the prior entry.

Charge pending loads against capacity size-background-fetch

import { LRUCache } from 'lru-cache';

const cache = new LRUCache({
  maxSize: 1_000,
  backgroundFetchSize: 25,
  sizeCalculation: (value) => value.cost,
  fetchMethod: loadValue,
});

Version 11.5 counts an unshadowed in-flight fetch as 25 size units here instead of the default 1.

Clean up after eviction dispose-evicted-values

const cache = new LRUCache({
  max: 100,
  disposeAfter(value, key, reason) {
    value.close?.();
    console.log({ key, reason });
  },
});

`disposeAfter` runs after the cache mutation, which is safer than changing the same cache inside `dispose`.

Reuse the same object key use-object-keys

const requestKey = { tenant: 'acme', id: 42 };
cache.set(requestKey, result);

cache.get(requestKey);               // hit
cache.get({ tenant: 'acme', id: 42 }); // miss

Object keys use reference identity, so 2 objects with equal fields do not address the same entry.

Record why a lookup missed inspect-operation-status

const status = {};
const value = cache.get('user:42', { status });
console.log({ value, status });

The status object is mutated with hit, miss, stale, TTL, size, and cache details; collecting it adds allocation cost.

Subscribe to synchronous metrics observe-node-cache

import { subscribe } from 'node:diagnostics_channel';

subscribe('lru-cache:metrics', (status) => {
  metrics.count(`cache.${status.get ?? status.set ?? 'operation'}`);
});

The `lru-cache:metrics` channel covers synchronous operations; async `fetch()` uses the separate tracing channel lifecycle.

Inject a deterministic clock test-ttl-clock

let now = 0;
const cache = new LRUCache({
  max: 10,
  ttl: 1_000,
  perf: { now: () => now },
});
cache.set('key', 'value');
now = 1_001;
expect(cache.get('key')).toBeUndefined();

The `perf` option avoids import-time capture of the real clock and makes the 1,000 ms expiry deterministic in tests.

Alternatives

PackageRegistryPick it when
quick-lrunpmUse it for a smaller count-bounded cache when async fetches, size accounting, and diagnostics are unnecessary.
mnemonistnpmUse its LRU structures when keys are short strings or integers and a narrower API is enough.
flat-cachenpmUse it when cache entries must persist to disk between process runs.

More utils guides

type-fest · ajv · p-limit · find-up · js-yaml · zod · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.