| name | caching-memory-efficiency |
| description | Production caching patterns and memory efficiency laws from 5 repos. In-memory TTL caches, unified KV interfaces, LRU eviction math, consistent hashing for distributed load, and GC thrash prevention via object pooling. Sources: node-cache/node-cache, jaredwray/keyv, isaacs/node-lru-cache, memcached/memcached, tweenjs/tween.js. |
/caching-memory-efficiency
When to Use
- Data pipeline code that hits the same DB/API keys repeatedly
- Choosing between in-process, distributed, or tiered cache
- Object allocation hot paths causing GC pause spikes
- "This service is slow but CPU is idle" — usually a cache miss pattern
Do NOT use for
- Single-request scripts (no repeated access)
- Data that must never be stale (financial ledgers, auth tokens)
Cache Strategy Decision Tree
Same process? → node-cache (in-memory, TTL)
Multi-process, 1 node? → keyv + sqlite/redis adapter
Multi-node? → keyv + Redis adapter OR memcached
Hot loop objects? → object pool (tween.js pattern)
Unbounded size risk? → lru-cache (auto-evict LRU entries)
In-Memory TTL Cache (node-cache)
import NodeCache from 'node-cache'
const cache = new NodeCache({ stdTTL: 60, checkperiod: 120, useClones: false })
async function getUser(id) {
const cached = cache.get(`user:${id}`)
if (cached) return cached
const user = await db.users.findById(id)
cache.set(`user:${id}`, user, 300)
return user
}
Unified KV Interface (keyv)
import Keyv from 'keyv'
import KeyvRedis from '@keyv/redis'
const cache = process.env.REDIS_URL
? new Keyv({ store: new KeyvRedis(process.env.REDIS_URL), ttl: 300_000 })
: new Keyv({ ttl: 300_000 })
await cache.set('session:abc', { userId: 42 })
const session = await cache.get('session:abc')
await cache.delete('session:abc')
const userCache = new Keyv({ namespace: 'user', ttl: 60_000 })
LRU Eviction (lru-cache)
import { LRUCache } from 'lru-cache'
const cache = new LRUCache({
max: 500,
maxSize: 50 * 1024 * 1024,
sizeCalculation: (value) => Buffer.byteLength(JSON.stringify(value)),
ttl: 1000 * 60 * 5,
allowStale: false,
})
function memoize(fn, keyFn) {
return (...args) => {
const key = keyFn(...args)
if (cache.has(key)) return cache.get(key)
const result = fn(...args)
cache.set(key, result)
return result
}
}
Consistent Hashing (memcached distribution principle)
class ConsistentHashRing {
constructor(nodes, replicas = 150) {
this.ring = new Map()
for (const node of nodes) {
for (let i = 0; i < replicas; i++) {
const hash = this.#hash(`${node}:${i}`)
this.ring.set(hash, node)
}
}
this.sortedKeys = [...this.ring.keys()].sort((a, b) => a - b)
}
getNode(key) {
const h = this.#hash(key)
const idx = this.sortedKeys.findIndex(k => k >= h) ?? 0
return this.ring.get(this.[idx % ..])
}
#() { }
}
GC Thrash Prevention — Object Pool (tween.js pattern)
class ObjectPool {
#free = []
#factory
constructor(factory, prewarm = 100) {
this.#factory = factory
for (let i = 0; i < prewarm; i++) this.#free.push(factory())
}
acquire() {
return this.#free.length ? this.#free.pop() : this.#factory()
}
release(obj) {
Object.keys(obj).forEach(k => { obj[k] = null })
this.#free.push(obj)
}
}
const tweenPool = new ObjectPool(() => ({ start: 0, end: 0, duration: 0 }), 200)
Anti-Fake-Pass Checklist
❌ Cache key without namespace (collision between modules)
❌ In-memory cache without max size / TTL (unbounded growth → OOM)
❌ useClones: true on high-frequency hot path (silent 2-3× slowdown)
❌ Distributing across nodes without consistent hashing (thundering herd on node add)
❌ Object pool that forgets to reset state on release (stale data leak)
❌ LRU cache with sizeCalculation absent (max: 500 = item count, ignores item size)