LRU Cache vs node-cache: como escolher o cache em memória certo no Node.js cover image

LRU Cache vs node-cache: como escolher o cache em memória certo no Node.js

Todo aplicativo Node.js acaba esbarrando no mesmo problema: uma query pesada no banco, uma chamada externa ou um cálculo caro que roda com frequência demais. Cache em memória é uma das formas mais rápidas de reduzir latência e proteger serviços downstream. Você guarda o resultado na RAM e reutiliza na próxima requisição, em vez de repetir o trabalho.

Duas bibliotecas dominam esse cenário no ecossistema Node.js: lru-cache (LRU cache) e node-cache. Ambas mantêm dados na memória do processo, são amplamente usadas, mas resolvem problemas ligeiramente diferentes. Escolher a errada pode gerar dados desatualizados, vazamento de memória ou eviction imprevisível.

Este post compara as duas de forma prática — o tipo de decisão que você toma ao integrar cache em um proxy de API Laravel, uma server route do Nuxt ou um microsserviço Node.js.

O que o cache em memória realmente resolve

Antes de comparar bibliotecas, vale definir o que você quer otimizar. Cache em memória funciona bem para:

  • Leituras caras — agregações no banco, resolvers GraphQL, respostas de APIs de terceiros
  • Proteção contra rate limit — evitar chamadas repetidas a serviços com cota restrita
  • Dados temporários — tokens de curta duração, feature flags, permissões calculadas
  • Deduplicação de requisições — evitar thundering herd quando muitos clientes pedem o mesmo recurso

Elas não substituem Redis, Memcached ou CDN quando você precisa de estado compartilhado entre instâncias. A memória do processo é local. Com três réplicas atrás de um load balancer, cada uma mantém seu próprio cache, a menos que você adicione uma camada distribuída.

Essa limitação é aceitável em muitos cenários. Uma camada SSR do Nuxt cacheando payloads de CMS, ou um BFF Node.js guardando fragmentos de perfil por alguns segundos, costuma ganhar muito com cache in-process, sem a complexidade operacional do Redis.

Entendendo o lru-cache

O lru-cache implementa eviction Least Recently Used (LRU). Quando o cache atinge o tamanho máximo — por quantidade de entradas ou tamanho aproximado em bytes — o item acessado há mais tempo é removido para abrir espaço.

A biblioteca é propositalmente enxuta. Foca em operações rápidas de get/set/delete e em limites previsíveis de memória. Suporta TTL, mas o modelo mental é "limite de tamanho primeiro": você define quanto cabe no cache e o LRU cuida do resto.

Uma configuração típica fica assim:

import { LRUCache } from 'lru-cache'

const cache = new LRUCache<string, UserProfile>({
  max: 500,
  ttl: 1000 * 60 * 5, // 5 minutos
  updateAgeOnGet: true,
})

function getProfile(userId: string): UserProfile | undefined {
  const hit = cache.get(userId)
  if (hit) return hit

  const profile = fetchProfileFromDb(userId)
  cache.set(userId, profile)
  return profile
}

Características principais do lru-cache:

  • Eviction por padrão de acesso — chaves quentes permanecem; chaves frias saem quando falta espaço
  • Teto rígido de memória — ideal quando você precisa garantir que o processo não cresça sem controle
  • Sem timers em background por padrão — entradas expiradas são limpas de forma lazy, no acesso ou ao liberar espaço
  • Ótimo suporte a TypeScript — generics para chaves e valores funcionam bem em bases tipadas
  • Alta performance — otimizado para milhões de operações por segundo em hot paths

Se sua preocupação principal é "tenho um conjunto limitado de dados e quero manter os mais usados na memória", o lru-cache é a escolha natural.

Entendendo o node-cache

O node-cache adota outra abordagem. Ele é construído em torno de semântica TTL-first. Cada chave pode ter expiração própria, e a biblioteca executa verificações periódicas para remover entradas expiradas em background.

Enquanto o lru-cache pergunta "o que remover quando estiver cheio?", o node-cache pergunta "quando esse valor deixa de ser válido?". Limites de tamanho existem, mas não são o centro do design.

import NodeCache from 'node-cache'

const cache = new NodeCache({
  stdTTL: 300,       // padrão: 5 minutos
  checkperiod: 60,   // limpa expirados a cada 60 segundos
  useClones: false,  // retorna referências por performance
})

function getProfile(userId: string): UserProfile | undefined {
  const hit = cache.get<UserProfile>(userId)
  if (hit) return hit

  const profile = fetchProfileFromDb(userId)
  cache.set(userId, profile, 300)
  return profile
}

Características principais do node-cache:

  • TTL como contrato principal — expiração explícita e override por chave é simples
  • Limpeza em background — um timer remove entradas obsoletas mesmo sem novo acesso
  • Event hooks — escute eventos set, del, expired e flush para logs ou métricas
  • Comportamento de clone — pode clonar valores no get/set para evitar mutação acidental (configurável)
  • Listagem e estatísticas — métodos nativos para inspecionar chaves, TTL restante e contadores

O node-cache lembra um Redis leve dentro do processo. Se sua regra de negócio depende de "esse valor vale por exatamente N segundos", o modelo encaixa bem.

Comparativo de recursos

  • Estratégia de eviction — lru-cache: LRU por ordem de acesso. node-cache: expiração por TTL; limite opcional de chaves sem ordenação LRU.
  • Controle de memória — lru-cache: nativo via max e maxSize. node-cache: depende de TTL e limites opcionais; crescimento ilimitado é possível se chaves não expiram.
  • Tratamento de TTL — lru-cache: suportado, limpeza lazy. node-cache: recurso central com remoção proativa.
  • Performance — lru-cache: extremamente rápido, overhead mínimo. node-cache: um pouco mais pesado por causa de timers e clone opcional.
  • Observabilidade — lru-cache: relatórios básicos de tamanho. node-cache: estatísticas e eventos mais ricos.
  • TypeScript — lru-cache: tipos nativos excelentes. node-cache: funciona com tipos, mas generics menos refinados.
  • Manutenção — lru-cache: ativamente mantido, adotado por npm e diversos frameworks. node-cache: estável, porém com releases menos frequentes.

Performance e memória em produção

Em benchmarks e hot paths reais, o lru-cache costuma vencer em throughput de get/set. Isso importa quando o cache fica em middleware que roda a cada requisição — por exemplo, validar claims de JWT ou resolver configuração de tenant em uma API multi-tenant.

O node-cache troca um pouco de velocidade por conveniência. Timers de checkperiod evitam que chaves expiradas fiquem na memória até alguém lê-las. Em serviços de longa duração com entradas que expiram e nunca mais são acessadas — tokens OAuth temporários, códigos de reset de senha — essa limpeza proativa reduz drift de memória ao longo de dias ou semanas.

Cuidado com useClones: true no node-cache. Clonar objetos grandes a cada get/set pode eliminar o ganho do cache. Use false quando tratar valores como imutáveis — a mesma disciplina que você aplicaria com lru-cache.

No lru-cache, o principal risco é TTL mal configurado com max alto. Entradas expiradas podem ocupar slots até serem acessadas, prejudicando chaves quentes. Ajustar ttlAutopurge (quando disponível na sua versão) ou manter max conservador resolve.

Quando escolher lru-cache

Prefira lru-cache quando:

  • Existir um conjunto limitado de dados quentes — top N produtos, buscas recentes, sessões ativas em um único processo
  • A frequência de acesso deve definir o que permanece cacheado, não só a idade
  • Você precisa de uso previsível de memória em containers com limite apertado
  • Performance é crítica — o cache está em loop apertado ou rota de API de alto QPS
  • Você constrói infraestrutura de biblioteca (data loader, wrapper de cache de ORM) onde dependências mínimas e velocidade importam

Um padrão comum em server routes do Nuxt: cachear payloads de CMS por slug com teto de 500 entradas. Páginas populares ficam quentes; páginas de cauda longa saem naturalmente. O TTL garante frescor sem precisar de um sweeper dedicado.

Quando escolher node-cache

Prefira node-cache quando:

  • TTL for o mecanismo principal de correção — códigos de auth, tokens de verificação de e-mail, URLs temporárias de upload
  • Você quiser eventos de expiração para logs, warming ou invalidação de chaves relacionadas
  • Precisar inspecionar TTL restante por chave em debug ou painel admin
  • O tamanho do cache for naturalmente limitado por dados de curta vida, não por padrão de acesso
  • Preferir uma API estilo Redis (ttl, keys, getStats) sem subir Redis

Em um setup híbrido Laravel + Node.js, o node-cache funciona bem em middleware Node que cacheia respostas de token OAuth de um identity provider — cada token tem expiry conhecido, e a limpeza em background mantém o processo enxuto.

Dá para combinar com Redis?

Sim, e muitas arquiteturas de produção fazem isso. Use cache in-process como L1 (latência de microssegundos) e Redis como L2 (milissegundos, compartilhado entre instâncias). O lru-cache costuma ser o melhor L1 pela velocidade e pelo teto de memória; o node-cache também serve quando o alinhamento de TTL com tokens upstream é a prioridade.

Em apps Nuxt com múltiplas instâncias edge ou server, nunca assuma que cache in-process sozinho garante consistência. Trate como camada de otimização. Invalide com agressividade ou mantenha TTLs curtos quando staleness for custoso.

Checklist prático de decisão

Antes de instalar qualquer uma das bibliotecas, responda:

  • Crescimento ilimitado de memória pode derrubar meu container? → prefira lru-cache com max explícito
  • A validade dos dados é definida por tempo, não por popularidade? → prefira node-cache
  • Preciso de eventos quando chaves expiram? → node-cache
  • Isso está no hot path da aplicação? → lru-cache
  • O cache precisa ser compartilhado entre instâncias? → nenhuma das duas sozinha; adicione Redis
  • Objetos cacheados são grandes ou mutáveis após leitura? → revise clone e prefira imutabilidade

Conclusão

lru-cache e node-cache não são substitutos intercambiáveis. Representam filosofias diferentes: eviction LRU versus expiração centrada em TTL. Ambos são escolhas válidas e testadas em produção no ecossistema Node.js.

Para a maioria das cargas de API e SSR — leituras rápidas, memória limitada e dados populares sempre por perto — o lru-cache é a recomendação padrão. Quando semântica de expiração, limpeza em background e observabilidade guiam o design — especialmente para artefatos de segurança ou sessão de curta duração — o node-cache faz sentido.

Escolha a biblioteca que reflete como seus dados realmente ficam obsoletos, meça hit rate em produção e mantenha TTLs honestos. Um cache em memória bem escolhido pode cortar milissegundos de cada requisição sem adicionar complexidade de infraestrutura.