Programming
Caching data based on how often it changes

A cache's expiration time is a guess about how soon its data might change. Set it too short and you keep fetching the same result. Set it too long and you can keep serving an old one.
I wanted a content-aware cache that could adjust that guess itself. Data that changes infrequently, or never changes, should be cached longer than something that changes on every refresh.
I built adaptive-cache to make that decision from the content being cached. It works with web responses and application data, using what it observes about changes to tune the cache lifetime.
Adjusting the cache lifetime
TTL, or time to live, is how long a cached value stays fresh. With the default adaptive policy, the cache starts with a short TTL. When freshly loaded data has the same hash as the previous value, it increases the TTL, up to a configured maximum. When the content changes, it resets the TTL to the starting value.
The cache keeps metadata about the value, including its hash, when it last changed, how many changes it has observed, and the learned TTL. Redis hashes hold that metadata, and Lua scripts update the cached data and metadata atomically. Multiple workers or servers can use the same cache without racing through separate read-and-write steps to update that history.
Seeing the TTL change
This example uses the current package API, Node.js 22 or newer, and a running Redis server. Install the dependencies:
npm install @0xdoublesharp/adaptive-cache ioredis
Save this as app.mjs. Each call to set() represents writing a freshly loaded value to the cache:
import Redis from 'ioredis';
import { AdaptiveCache } from '@0xdoublesharp/adaptive-cache';
const redis = new Redis(process.env.REDIS_URL ?? 'redis://127.0.0.1:6379');
const cache = new AdaptiveCache({
keyPrefix: 'adaptive-example:',
initialTTL: 5,
maxTTL: 60,
ttlScaling: 2,
}, redis);
const key = 'adaptive-example:summary';
try {
await cache.clear(key);
const first = await cache.set(key, { count: 42 });
const unchanged = await cache.set(key, { count: 42 });
const changed = await cache.set(key, { count: 43 });
console.log(first[1], unchanged[1], changed[1]);
// 5 10 5, in seconds
} finally {
await cache.clear(key);
await cache.close();
await redis.quit();
}
Run it with node app.mjs. Set REDIS_URL if Redis is running somewhere other than the default local address. The output is:
5 10 5
The first value gets a five-second TTL. Writing the same content increases it to ten seconds. Changing the count from 42 to 43 brings it back to five seconds. Further unchanged writes can grow the TTL toward the sixty-second maximum.
A cache hit does not count as a new observation. The cache learns about a change when fresh data is written; it does not watch the source for updates. If the application already knows a value has changed, it can invalidate the key or its tags directly.
More ways to tune the TTL
The original adaptive policy remains the default. The package now also supports AIMD, which increases the TTL in fixed steps and reduces it by a factor when content changes, and EWMA, which estimates change frequency while giving more weight to recent observations.
These policies let you choose how the cache responds to the changes it observes. Their behavior and settings are covered in the TTL policy guide.
Across workers and servers
Redis shares cached data and metadata between servers. For frequently used data, adaptive-cache can also use lru-cache-clustered to keep payloads in a bounded cache shared by workers on one host.
In l1-redis mode, Redis remains authoritative. A local hit still checks invalidation state against Redis, but can avoid fetching and decoding the full payload. This combines efficient storage within a Node.js cluster with shared state across servers.
There is also a clustered-lru backend for a single server or cluster that does not need Redis. Its state stays on that host, so it does not provide the same cross-server coordination.
The package includes Express and Fastify integrations as well as the direct API shown here. You can find it on npm, with the source and more examples on GitHub.