weak-lru-cache
weak-lru-cache is an in-memory JavaScript cache that combines a strongly retained least-recently and frequently used window with WeakRef storage after an object ages out of that window. Recently used objects stay predictable; older objects may remain retrievable until the garbage collector reclaims them. It is designed for object identity reuse and memory-sensitive memoization, not for TTL expiry, persistence, cross-process sharing, or exact capacity guarantees.
A clever fit for recreatable object graphs whose cold values may vanish whenever the runtime chooses. Most application caches need deterministic limits or TTLs instead, and should install lru-cache rather than accept weak-reference semantics and a surprising Map interface.
Use it if
- You cache recreatable object values and want memory pressure to decide when cold entries finally disappear
- You need to return the same object identity for a key while that object remains alive elsewhere
- You run Node 14.10 or newer, or another runtime with WeakRef and FinalizationRegistry support
- You can tolerate cache misses at nondeterministic times and will always recompute safely
- You need deterministic TTLs, max age, or exact eviction timing: the API exposes recency and frequency retention plus garbage collection, not wall-clock expiration
- Your cache holds primitive values: primitives cannot be weakly referenced, so the README says they are removed as soon as they leave the strong retention cache
- You need an exact maximum number of live objects or bytes: weak collection timing belongs to the runtime, and expirationPriority is only a relative retention hint
- You want ordinary Map semantics: WeakLRUCache extends Map, but get returns an internal cache entry while getValue returns your value, which makes generic Map code unsafe
- You want active releases and polished examples: 1.2.2 was published in February 2022, the repository last moved in April 2022, and the README's setup sample refers to myValue without defining it
Setup reality
There are no dependencies, peers, native builds, credentials, or config files: install weak-lru-cache and import WeakLRUCache. Runtime support is the first real gate. The README requires Node 14.10 or newer, unless using the old Node 13 harmony flag, because the implementation constructs WeakRef and FinalizationRegistry immediately. The package provides ESM and CommonJS entries and a small index.d.ts, but that declaration says setValue returns void while the implementation returns a CacheEntry, so do not build typed code around the runtime return value. The biggest surprise is its Map inheritance. setValue and getValue are the intended value API; inherited get returns the internal WeakRef-like CacheEntry, iteration yields entries rather than values, and inherited set expects one of those internal entries rather than an ordinary object. Objects start with a strong reference inside the LRFU expirer, then fall back to a weak reference after expiration; collection after that is nondeterministic. Primitives cannot take the weak phase. cacheSize defaults to 32,768 and accepts at most 16,777,216 according to the README, but it controls the strong retention machinery rather than an exact object ceiling. A shared expirer is the default across cache instances. Pass expirer: false for weak-only behavior, use a custom LRFUExpirer only when you understand that cross-cache retention tradeoff, and never write tests that force or time garbage collection as correctness behavior.
Patterns
Store and retrieve an objectcache-object
import { WeakLRUCache } from 'weak-lru-cache';
const cache = new WeakLRUCache();
cache.setValue('user:42', { id: 42, name: 'Ada' });
const user = cache.getValue('user:42');The object is strongly retained while active, then weakly retained after LRFU expiration and may disappear after garbage collection.
Recreate a value after a cache missfill-on-miss
async function getUser(id) {
const key = `user:${id}`;
let user = cache.getValue(key);
if (user === undefined) {
user = await database.loadUser(id);
cache.setValue(key, user);
}
return user;
}A miss is always possible, even if the key was stored earlier, so recomputation must be safe and expected.
Intern objects by a stable keypreserve-object-identity
function internNode(id, fields) {
const existing = cache.getValue(id);
if (existing) return existing;
const node = Object.freeze({ id, ...fields });
cache.setValue(id, node);
return node;
}Identity is preserved only while the object remains cached or strongly referenced elsewhere; it is not permanent interning.
Tune the strong retention cacheset-cache-size
import { WeakLRUCache } from 'weak-lru-cache';
const cache = new WeakLRUCache({
cacheSize: 4096,
});cacheSize tunes the LRFU retention structure, not an exact cap on weakly reachable objects or memory bytes.
Expire a large object soonerweight-large-value
const bytes = Buffer.byteLength(JSON.stringify(document));
cache.setValue(document.id, document, bytes >> 10);Higher expirationPriority values expire sooner. The README suggests approximate kilobytes as a relative weight, not as byte-accurate accounting.
Pin a value in strong memorypin-value
cache.setValue('schema', compiledSchema, -1);A negative expiration priority pins the entry until it is changed; pinning defeats the memory-release reason for choosing a weak cache.
Replace a pinned value with normal retentionunpin-value
const current = cache.getValue('schema');
if (current) {
cache.setValue('schema', current, 0);
}Calling getValue alone keeps a pinned entry pinned. Replace it with a nonnegative priority to return it to normal expiration.
Disable the strong LRFU layeruse-weak-only-mode
const weakOnly = new WeakLRUCache({
expirer: false,
});
weakOnly.setValue('preview', previewObject);With no strong retention cache, values may become collectible as soon as outside strong references disappear. Primitive values are especially poor fits.
Give one cache a separate expirerisolate-expiration-policy
import { LRFUExpirer, WeakLRUCache } from 'weak-lru-cache';
const expirer = new LRFUExpirer({
lruSize: 2048,
cleanupInterval: 30000,
});
const cache = new WeakLRUCache({ expirer });The default expirer is shared. A private expirer isolates competition but gives up automatic priority across caches.
Remove an entry explicitlydelete-entry
const removed = cache.delete('user:42');Explicit deletion is deterministic and also removes the entry from the expiration structure; do this for invalidation instead of waiting for GC.
Clear all entriesclear-cache
cache.clear();clear removes entries from both the Map and the LRFU expirer, but objects can of course remain alive through references held elsewhere.
Inspect internal entry metadata deliberatelyinspect-cache-entry
const entry = cache.get('user:42');
const value = entry?.deref ? entry.deref() : entry?.value;Inherited get returns a CacheEntry, not your cached value. Application code should normally call getValue and avoid depending on internal fields.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| lru-cache | npm | You need maintained deterministic limits, TTLs, disposal hooks, and fetch-style cache filling |
| quick-lru | npm | You want a compact ESM LRU with a predictable maximum size and simple API |
| mnemonist | npm | You need a broader collection library that includes fixed-capacity LRU maps and caches |