LRU, de Least Recently Used, é uma política de cache que remove primeiro os itens menos usados recentemente. Em aplicações Node.js, um cache LRU local pode reduzir chamadas repetidas ao banco, parsing, leitura de arquivos e cálculos, mantendo um limite de memória.
Um Map simples sem limite cresce até pressionar o heap. LRU adiciona capacidade máxima, expiração e descarte previsível. Ele é adequado para dados descartáveis e reconstruíveis, não para estado de negócio.
Quando usar
Casos comuns:
- configurações carregadas repetidamente;
- metadados de arquivos;
- resultados de parsing;
- permissões com TTL curto;
- respostas internas pequenas;
- schemas compilados;
- resolução de chaves públicas;
- dados de referência.
Quando não usar
Não use como:
- banco de dados;
- sessão única sem persistência;
- fila durável;
- controle global entre pods;
- fonte de saldo ou estoque;
- armazenamento de objetos enormes sem limite por tamanho.
LRU simples com Map
Map preserva ordem de inserção. Ao acessar, remova e insira novamente:
class LruCache {
constructor(maxEntries = 1000) {
this.maxEntries = maxEntries;
this.map = new Map();
}
get(key) {
if (!this.map.has(key)) return undefined;
const value = this.map.get(key);
this.map.delete(key);
this.map.set(key, value);
return value;
}
set(key, value) {
if (this.map.has(key)) this.map.delete(key);
this.map.set(key, value);
if (this.map.size > this.maxEntries) {
const oldestKey = this.map.keys().next().value;
this.map.delete(oldestKey);
}
}
delete(key) {
return this.map.delete(key);
}
clear() {
this.map.clear();
}
}Essa implementação limita entradas, mas não TTL, tamanho em bytes ou eventos de descarte.
TTL
class TtlLruCache {
constructor(maxEntries = 1000, ttlMs = 60_000) {
this.maxEntries = maxEntries;
this.ttlMs = ttlMs;
this.map = new Map();
}
set(key, value, ttlMs = this.ttlMs) {
const entry = {
value,
expiresAt: Date.now() + ttlMs,
};
if (this.map.has(key)) this.map.delete(key);
this.map.set(key, entry);
this.evict();
}
get(key) {
const entry = this.map.get(key);
if (!entry) return undefined;
if (entry.expiresAt <= Date.now()) {
this.map.delete(key);
return undefined;
}
this.map.delete(key);
this.map.set(key, entry);
return entry.value;
}
evict() {
while (this.map.size > this.maxEntries) {
const oldestKey = this.map.keys().next().value;
this.map.delete(oldestKey);
}
}
}Expiração preguiçosa
O exemplo remove entradas expiradas quando são lidas. Uma chave nunca acessada continua ocupando espaço até ser removida por capacidade. Isso é aceitável quando maxEntries é rígido.
Limpeza periódica
Um timer pode remover expirados:
const timer = setInterval(() => {
cache.purgeExpired();
}, 60_000);
timer.unref();Não percorra milhões de entradas em um único tick. Faça limpeza incremental ou use biblioteca otimizada.
Limite por tamanho
Mil strings pequenas e mil Buffers de 10 MB não são equivalentes. Calcule peso:
function sizeOf(value) {
if (Buffer.isBuffer(value)) return value.byteLength;
if (typeof value === 'string') return Buffer.byteLength(value);
return Buffer.byteLength(JSON.stringify(value));
}Serializar para medir custa CPU e pode falhar em objetos cíclicos. Prefira tamanho conhecido no momento da criação.
maxSize
Uma implementação por peso mantém soma de bytes estimados e remove itens até ficar abaixo do limite.
Objetos mutáveis
Se o cache devolve a mesma referência, o chamador pode alterar o valor armazenado:
const user = cache.get('user:42');
user.role = 'admin';Use objetos imutáveis, clone ou documente que valores não devem ser mutados.
Chaves
Uma chave precisa representar todos os parâmetros que alteram o resultado:
const key = JSON.stringify({
userId,
locale,
fields: [...fields].sort(),
});Não inclua objeto sem canonicalização. Ordem diferente pode gerar misses.
Alta cardinalidade
IDs arbitrários de usuário podem preencher o cache com itens acessados uma vez e expulsar dados quentes. Use:
- capacidade adequada;
- admission policy;
- TTL curto;
- limite por tenant;
- não cachear resultados únicos;
- cache segmentado.
Cache pollution
Uma varredura por milhares de chaves pode remover itens realmente populares. Políticas avançadas consideram frequência além de recência. Bibliotecas maduras podem oferecer algoritmos melhores.
Get or load
async function getOrLoad(key, loader) {
const cached = cache.get(key);
if (cached !== undefined) return cached;
const value = await loader();
cache.set(key, value);
return value;
}Esse código sofre stampede em concorrência.
Promise em andamento
const inFlight = new Map();
async function getOrLoad(key, loader) {
const cached = cache.get(key);
if (cached !== undefined) return cached;
if (inFlight.has(key)) return inFlight.get(key);
const promise = loader()
.then((value) => {
cache.set(key, value);
return value;
})
.finally(() => inFlight.delete(key));
inFlight.set(key, promise);
return promise;
}Use timeout e limite o mapa.
Cachear undefined
Se undefined representa miss e também valor válido, use sentinel:
const NOT_FOUND = Symbol('not-found');Ou retorne um objeto com found.
Negative caching
Cacheie ausência por TTL curto:
cache.set(key, { found: false }, 10_000);Invalide depois de criar o recurso.
Stale-while-revalidate local
Armazene freshUntil e staleUntil. Durante stale, devolva o valor e atualize em background. Isso reduz picos e latência.
Invalidação
Quando um dado muda:
await repository.update(id, input);
cache.delete(`product:${id}`);Se a atualização e invalidação não são atômicas, TTL limita a inconsistência. Eventos podem invalidar outras réplicas.
Múltiplos processos
Cada processo cluster e Worker Thread possui cache próprio. Uma invalidação local não chega aos demais. Use Pub/Sub ou TTL curto.
Dois níveis
L1 local e L2 Redis:
L1 LRU → Redis → bancoL1 deve ser pequeno e curto. L2 oferece compartilhamento, mas adiciona rede.
Warmup
Pré-carregue poucas entradas críticas. Um warmup enorme aumenta startup e memória. Readiness só deve depender de dados realmente essenciais.
Heap
O cache vive no heap JavaScript. Valores também podem reter buffers e grafos grandes. Monitore heapUsed, RSS e external memory.
Garbage collection
Um cache próximo ao limite do heap aumenta GC. Não dimensione apenas pelo limite máximo do V8. Reserve espaço para tráfego, código e picos.
WeakMap
WeakMap não é um cache LRU: só aceita objetos como chaves, não permite enumerar e não controla recência. É útil para associar metadados sem impedir coleta do objeto.
Bibliotecas
Bibliotecas como lru-cache oferecem TTL, maxSize, fetch method, dispose e otimizações. Fixe versão, leia o contrato e teste comportamento de stale e abort.
Dispose
Ao remover valor que possui recurso, execute limpeza:
function dispose(value) {
value.close?.();
}Não armazene conexões de banco em LRU; use pool apropriado.
Segurança
Não misture dados entre usuários. Inclua tenant na chave, limite acesso e evite armazenar tokens, senhas ou payloads sensíveis.
Timing
Cache pode criar diferenças de tempo observáveis. Não use resultado cacheado para validações secretas sem analisar risco.
Observabilidade
Meça:
- hit e miss;
- hit ratio;
- entradas;
- tamanho estimado;
- evictions;
- expirações;
- loads;
- coalesced requests;
- load errors;
- stale hits;
- tempo economizado.
Hit ratio não basta
Um cache pode ter hit alto em operações baratas e baixo em operações caras. Meça custo evitado e impacto de memória.
Teste
Teste recência, substituição, TTL, peso, valores mutáveis, concorrência, erro do loader, negative cache e invalidação.
Benchmark
Compare sem cache e com diferentes tamanhos. Meça p99, CPU, heap, GC e origem. Um cache maior pode piorar desempenho por GC.
Erros comuns
- usar Map sem limite;
- limitar apenas quantidade com objetos enormes;
- não considerar tenant na chave;
- devolver objeto mutável;
- não controlar stampede;
- cachear erro transitório;
- não invalidar;
- achar que cache local é compartilhado;
- preencher tudo no startup;
- não medir GC.
Fluxo recomendado
Defina maxEntries ou maxSize, TTL e política de chaves. Use request coalescing e mantenha o cache descartável. Combine com Cache Stampede, Cache HTTP, Redis com Node.js e Heap Snapshots.
Consulte a documentação do projeto lru-cache e a referência de Map.



