Augusto Dalmas
notas de estudo
← voltar aos artigos
System Design  ·  30 jun 2026  ·  6 min de leitura

Cache-aside em Node.js: o que muda de verdade entre 1000 requisições sem e com cache

Quanto de carga eu tiro do banco quando 1000 pessoas pedem a mesma coisa ao mesmo tempo? Foi essa a pergunta que motivou esse teste.

Montei um cenário concreto pra responder isso: uma API básica em Express, MongoDB rodando em Docker, e um dataset de 100 mil produtos pra ter volume de dado real.

A rota testada

A rota é simples, com paginação:

./controller/productsController.js

const productsController = {
    get: async function (req, res) {
        const page = parseInt(req.query.page) || 1;
        const limit = parseInt(req.query.limit) || 50;

        const skip = (page - 1) * limit;

        const result = await productsModels.findAll(skip, limit)

        res.status(200).json({
            page,
            limit,
            total: result.total,
            count: result.data.length,
            data: result.data
        })
    }
}

Em cima dela eu apliquei um middleware de cache usando a lib node-cache. Pra forçar um cenário realista de carga, testei com limit=100000 — na prática isso equivale a trazer a coleção inteira numa única página, o tipo de query pesada que, mesmo com paginação disponível, alguém vai acabar escrevendo assim mesmo sabendo que não deveria.

Os números

SituaçãoRequisições na APIQueries no banco
Sem cache10001000
Com cache10001

Em tempo: a consulta sem cache levou cerca de 40 segundos, e com cache caiu pra cerca de 22 — uma melhora real, mas longe de instantânea, porque o gargalo de trafegar ~110 mil documentos pela rede continua existindo mesmo com o dado em memória.

A coluna que importa aqui não é a de tempo, é a de queries: de 1000 requisições, só 1 encostou no banco. As outras 999 foram respondidas direto da memória do processo Node, por causa desse trecho do middleware:

./middleware/cache.js

const key = `${req.method}:${req.originalUrl}`;
const cached = cache.get(key);

if (cached) {
    return res.json(cached);
}

Por que isso importa mais que velocidade

O ganho de cache aqui não é velocidade, é proteção do banco contra carga repetida. Se 1000 usuários pedem a mesma coisa dentro da janela de TTL (60 segundos, no meu stdTTL), o MongoDB nem fica sabendo que 999 dessas requisições existiram. Isso importa mais pra escalabilidade do que economizar alguns milissegundos de resposta — um banco que recebe 1 query em vez de 1000 aguenta muito mais tráfego simultâneo antes de cair.

Limitações que ficaram evidentes

Esse experimento é propositalmente simples, e isso deixou claro onde ele quebra:

  • O cache vive na memória de um único processo Node. Se eu subir duas instâncias da API atrás de um load balancer, cada uma tem seu próprio cache — e o hit rate cai pela metade (ou mais).
  • Não existe invalidação ativa. Se um produto for atualizado no banco durante a janela de TTL, a API vai continuar servindo o dado antigo por até 60 segundos.
  • O TTL fixo é uma aposta. Curto demais e eu perco boa parte do benefício; longo demais e o risco de servir dado desatualizado cresce.

Próximo passo

O caminho óbvio daqui é trocar o node-cache (em memória, por processo) por Redis (compartilhado entre instâncias), e pensar em invalidação ativa em vez de só depender de TTL — invalidar a chave quando o dado muda, ao invés de esperar ele expirar sozinho.

#cache #mongodb #performance #estudo
próximo artigo →
RabbitMQ + Python: o que acontece quando o worker falha antes do ack