A história de um cache que não cacheava — e o que aprendemos com isso

A história de um cache que não cacheava — e o que aprendemos com isso

29 de out. de 2025 · Por Enzo Moraes ·
2 Minutos de leitura

(TL;DR): Atualmente, nossa API utiliza Grape integrado ao grape-rails-cache para cachear respostas de endpoints. Porém, a estratégia implementada não estava efetivamente armazenando ou reutilizando o cache das requisições, resultando em chamadas repetitivas e desnecessárias às APIs externas e consultas ao banco de dados.

⚠️ Problema Identificado

1. Implementação atual (seguindo padrões da lib)

resources :posts do
desc "Return a post"
get ":id" do
post = Post.find(params[:id])
cache(key: "api:posts:#{post.id}", etag: post.updated_at, expires_in: 2.hours) do
post # post.extend(PostRepresenter) etc, any code that renders response
end
end
end
end

2. Problema técnico

Seguindo a documentação da lib, realizamos a chamada da Interaction/Query/Request antes de abrir o bloco cache.

Isso faz com que todas as vezes, a chamada da Interaction/Query/Request seja realizada, o que faz o método cache do Grape não armazenar o resultado do bloco como esperado — o que significa que o conteúdo é recalculado a cada requisição. Na realidade estamos cacheando apenas a serialização da resposta e não a consulta ao banco…

3. Consequência

✅ Estratégia Corrigida

A implementação correta envolve utilizar as chamadas dentro do bloco cache, garantindo o armazenamento e reutilização dos dados:

resources :resource do
desc "Returns a response"
get "/" do
cache(key: "api:resource:xx", expires_in: 2.hours) do
response = Resource::FetchExternalResource.call
response
end
end
end
end

O ponto aqui é quando precisamos cachear uma query ou chamada externa, não apenas uma entidade. Não estamos lidando com HTTP Cache diretamente, mas sim com Server Side Cache. A documentação da lib não deixa isso explícito, o que foi meio que um missdirect, que fez com que o time implementasse sempre dessa forma, deixando as partes pesadas fora do bloco da cache.

Benefícios:

📊 Métricas (Datadog) - Exemplo real

Antes da mudança:

Performance Antes
Performance Antes

Depois da mudança:

Nem todas as requests vão ao banco de dados

obs: o tempo de expiração do cache do exemplo é curto de 3 min. Então a porcentagem ainda será elevada.

Performance Depois
Performance Depois

Porcentagem de tempo de espera

Repare que o tempo de espera do endpoint em relação a consultas ao banco começa a diminuir após a mudança, e por consequência o uso do cache começa a aumentar - de fato começa a ser usado.

Porcentagem de uso Memcached por request
Porcentagem de uso Memcached por request
Porcentagem de uso do Postgres por request
Porcentagem de uso do Postgres por request

Conclusão

A correção da estratégia de cache trouxe ganhos expressivos de performance e estabilidade, reduzindo tempo de resposta, carga nos serviços externos e custos operacionais.

Essa iniciativa reforça a importância de auditar implementações de caching e não confiar 100% na documentação de libs e entender se os casos de uso que ela expõe na documentação são os mesmos que queremos aplicar.

Acompanhar após as implementações durante um período para entender se a estratégia de cache está de fato sendo efetiva.