lru-cache: bounded LRU caching for Node.js, with TTL and size limits
A fast cache that automatically deletes the least recently used items
At a glance
- What is it?
- The lru-cache package keeps a fixed number of recently used items and evicts the rest. It is a good fit when you can name a bound up front, and the wrong tool when you only want time-based expiry.
- Who is it for?
- Adopt lru-cache when you can state a bound: a max item count, a maxSize with sizeCalculation, or a ttl, and when you want a synchronous in-process cache with optional stale-while-revalidate fetching. Do not adopt it as a TTL-first cache or as a shared cache between processes; the README points TTL-focused users at @isaacs/ttlcache and notes that a plain Map with setTimeout beats an LRU for pure expiry.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 12 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What problem lru-cache solves, and for whom
An unbounded in-process map is a memory leak with a friendly API. lru-cache exists to put a ceiling on that map and to choose what leaves when the ceiling is reached: the least recently used entries go first. The README states the contract plainly, that you specify a max number of the most recently used items you want to keep and the cache keeps that many of the most recently accessed items.
The audience is Node.js developers holding derived data in memory: parsed responses, compiled templates, database rows fetched by id, anything expensive to recompute and cheap to store. It is a library, not a service. There is no server, no port, no persistence layer, and no coordination between processes. That single-process scope is the whole design, and it is worth being honest about it before adopting anything.
The package also ships a browser build and a react-native dialect, visible in the package.json tshy configuration, so the same API can be used in front-end code. The README shows a minified ESM import from unpkg for that case.
How eviction, size accounting and TTL actually interact
The cache is built around a bound you must supply. At construction, storage is allocated for max items, which the README says is the fastest configuration because allocation happens up front. If you prefer to bound bytes rather than count, set maxSize and provide sizeCalculation, or pass a size per set call. Every item size must be a positive integer, and the README is explicit that this is a requirement, not a suggestion.
TTL is secondary here, and the README says so directly: this is not primarily a TTL cache and does not make strong TTL guarantees. Expired items are treated as missing and deleted when fetched, not swept in the background, unless ttlAutopurge is enabled. That distinction matters. A cache configured with only a ttl and no max or maxSize can grow without bound, and the README notes a warning is printed to standard error in that case, with a hint that future versions may require ttlAutopurge.
Two behaviours surprise people. First, undefined is never stored. Calling set with undefined is an alias for delete, so has returns false afterwards. The README suggests a Symbol sigil if you need to distinguish a stored undefined from a missing key. Second, non-string keys work but are matched by object identity, not by structural equality. The README's example sets an object key and a string key, then shows that a freshly constructed object with the same fields returns undefined.
For read-through patterns there is fetchMethod, an async function used by cache.fetch(), described in the README as the hook for stale-while-revalidate behaviour. Most methods also accept a status option that gets decorated during the operation, which is the observability path if you need to know why an entry was evicted.
Installing lru-cache and a first working cache
Installation is a single npm command. The README gives it as follows.
npm install lru-cache --saveThe package is a hybrid module, so either import style works. The README's usage example constructs a cache with a max item count, a ttl, and a dispose callback, then reads and writes a key. The important part is the constructor: the README states that at least one of max, ttl or maxSize is required to prevent unsafe unbounded storage.
import { LRUCache } from 'lru-cache'
const cache = new LRUCache({
max: 500,
ttl: 1000 * 60 * 5,
dispose: (value, key, reason) => {
freeFromMemoryOrWhatever(value)
},
})
cache.set('key', 'value')
cache.get('key') // "value"
cache.clear()After running that, get returns the string you set, and once 500 distinct keys have been inserted, the least recently used one falls out. If you are bounding by size instead of count, the README's shape looks like this, and every item must report a positive integer size.
const cache = new LRUCache({
maxSize: 5000,
sizeCalculation: (value, key) => {
return 1
},
})Where lru-cache is the wrong tool
The clearest failure mode is using it as a TTL cache. The README says there is no preemptive pruning of expired items by default, and that if you want a cache bound only by TTL expiration you should consider a Map with setTimeout, which it says will perform much better than an LRU cache. That is an unusually direct piece of advice from a library about its own limits, and it should be taken at face value. The README even includes a full implementation of such a Map-based cache under the same license.
A second boundary is process scope. Nothing in the repository describes a shared or distributed cache. Two Node processes each get their own LRUCache, with independent eviction and independent TTL clocks. If your problem is coherent caching across a fleet, this package does not address it.
The third is memory accounting accuracy. maxSize bounds what you tell it each item weighs. If sizeCalculation returns 1 for every item, you have a count bound wearing a byte bound's clothes, and a cache of large buffers will exceed whatever you thought you were limiting. The README requires positive integers but cannot verify that your estimate reflects real retention.
Finally, performance is not free. The README states that using some features necessarily impacts performance by making the cache do more work, and points readers to its Performance section. A cache with sizeCalculation, dispose, and ttlAutopurge is doing considerably more per operation than one with a plain max.
lru-cache compared with node-cache and a plain Map
node-cache is the other package people reach for in Node, and the difference in approach is worth stating precisely. node-cache is built around TTL as the primary expiry mechanism, with a check interval that sweeps expired keys, and a maxKeys option layered on top. lru-cache inverts that: recency is the primary policy, TTL is an optional overlay, and expired entries are reclaimed lazily on access unless you turn on ttlAutopurge. If your mental model is "entries expire after N seconds and the cache happens to have a size cap," node-cache matches it. If your model is "keep the hot set, bounded, and let age be a secondary signal," lru-cache matches it.
The plain Map comparison is the one the README itself makes, and it is not a competitor so much as a lower bound. A Map with setTimeout deletes on a timer, gives exact expiry, and skips all the bookkeeping an LRU needs. It has no size bound and no recency policy. For a small fixed set of keys with uniform lifetimes, the Map wins outright and the README says so.
For TTL-first work, the README points at @isaacs/ttlcache, a separate package by the same author. That is the intended escape hatch when lru-cache's guarantees are not the ones you need.
Maintenance, licence and what an upgrade costs you
The repository is not archived, and the last push was on 2026-09-18, so the project is being touched. The published version in package.json is 11.5.3. The package is written in TypeScript, built with tshy, tested with tap, and linted with oxlint, all visible in the scripts block. The build pipeline runs on prepare, so installing from git triggers a build rather than shipping prebuilt dist files alone.
The licence is BlueOak-1.0.0. That is a permissive licence, but it is not MIT or Apache-2.0, and it is not a licence many organisations have already approved in their allowlists. The practical consequence is that a legal or compliance review may take longer than it would for a more common identifier, and some automated dependency scanners may flag it as unknown rather than permit it. This is not legal advice, and the text in LICENSE.md is what governs.
Upgrade cost is mostly about the major version. The README frames version 7 as the point where the implementation became one of the most performant LRU implementations in JavaScript, and the current line is 11.x, so anyone on a pre-7 version is several majors behind. The API shown here, a named LRUCache export with an options object, is the modern shape; older code that called the constructor directly or used the default export will need rewriting. The typedocs at the project's homepage are the reference for the full option set, and the README defers to them rather than enumerating everything.
Editorial conclusion
Adopt lru-cache when you can state a bound: a max item count, a maxSize with sizeCalculation, or a ttl, and when you want a synchronous in-process cache with optional stale-while-revalidate fetching. Do not adopt it as a TTL-first cache or as a shared cache between processes; the README points TTL-focused users at @isaacs/ttlcache and notes that a plain Map with setTimeout beats an LRU for pure expiry. Before you commit, verify that your keys are stable object identities if you use non-string keys, that every item has a positive integer size when maxSize is set, and that you are not relying on undefined as a stored value.
Frequently asked questions
What is an LRU cache?
An LRU cache keeps a bounded set of items and, when it is full, deletes the least recently used ones first. In lru-cache you specify a max number of items to keep, and the cache retains that many of the most recently accessed entries.
When should I use lru-cache instead of a TTL cache?
Use lru-cache when recency is the policy you want and you can name a bound, such as max or maxSize. The README states it is not primarily a TTL cache and does not make strong TTL guarantees, and points TTL-focused users at @isaacs/ttlcache instead.
How do I install lru-cache in a Node project?
Run npm install lru-cache --save, then import the named LRUCache export. The constructor requires at least one of max, ttl or maxSize, otherwise storage is unbounded.
Can I use object keys with lru-cache?
Yes, but they are matched by identity rather than by value. The README shows that a different object with the same fields returns undefined, because it is not the same object reference.
Does lru-cache delete expired items automatically?
Not by default. The README says expired items are treated as missing and deleted when fetched, and that preemptive pruning requires enabling ttlAutopurge.
Which licence does lru-cache use?
The repository lists BlueOak-1.0.0, and the licence text lives in LICENSE.md. That is a permissive licence but not MIT or Apache-2.0, so it may need a separate review in organisations with an allowlist.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/isaacs-node-lru-cache)