Cache em APIs: Entenda os tipos, vantagens e quando usar
Cache em APIs é um dos principais recursos para melhorar a performance, reduzir custos e aliviar a carga do banco de dados em aplicações modernas. Por isso, entender como ele funciona é parte do trabalho de qualquer pessoa que projeta ou mantém um backend.
APIs que trabalham com dados compartilhados, como catálogos de filmes, listas de produtos ou páginas iniciais, costumam sofrer com consultas repetidas. Na maioria das vezes, essas consultas poderiam ser evitadas com pouco esforço.
Antes de implementar cache diretamente no código, porém, vale compreender quais tipos existem, quando cada um faz sentido e quais problemas eles realmente resolvem. Neste artigo, analisamos os principais tipos de cache em APIs, do cenário mais simples ao mais robusto.
O problema das consultas repetidas em APIs
Em um cenário básico, o fluxo de uma API costuma ser direto. O cliente envia a requisição, a API recebe e, em seguida, consulta o banco de dados.
Cliente → API → Banco de Dados
O problema surge quando vários usuários acessam exatamente a mesma informação. Mesmo sem nenhuma alteração nos dados, a API continua executando a mesma consulta no banco a cada requisição.
Esse padrão gera um impacto significativo, especialmente no banco de dados. Afinal, ele normalmente é o recurso mais caro da arquitetura e também o mais difícil de escalar.
Imagine, por exemplo, a página inicial de uma loja virtual com os produtos em destaque. Milhares de pessoas abrem essa página por hora, e a lista muda talvez uma vez por dia.
Sem nenhuma camada intermediária, cada visita dispara a mesma consulta, com os mesmos filtros e o mesmo resultado. Ou seja, o banco trabalha milhares de vezes para entregar uma resposta que já conhecia.
A questão central passa a ser outra: essa consulta realmente precisa rodar toda vez?
Por que o cache em APIs existe
O cache em APIs existe justamente para evitar esse desperdício. Ele permite reaproveitar dados que a aplicação já consultou antes, reduzindo acessos desnecessários ao banco de dados.
Conceitualmente, o cache guarda uma cópia do dado em uma camada intermediária, que responde muito mais rápido do que a origem. A documentação de caching do .NET define a técnica justamente assim: uma estratégia de otimização de performance e uma decisão de design.
Além de reduzir custos, o uso de cache melhora a performance da API e diminui o número de conexões simultâneas com o banco. Com isso, o sistema ganha escalabilidade e o usuário final recebe respostas mais rápidas.
Apesar de todos esses benefícios, o cache não serve para qualquer cenário. Ele resolve problemas específicos e, ao mesmo tempo, adiciona complexidade à aplicação.
Os principais benefícios do cache em APIs
O primeiro ganho aparece no tempo de resposta, porque ler da memória ou de um servidor de cache costuma ser bem mais rápido do que executar uma consulta completa. Em seguida vem a redução de carga, já que o banco deixa de receber consultas repetidas.
Também existe um ganho de custo. Em ambientes de nuvem, menos consultas significam instâncias de banco menores ou menos unidades de processamento contratadas.
Por fim, o cache ajuda na resiliência. Segundo as orientações de arquitetura da Microsoft, uma cópia em cache pode, de forma limitada, manter a disponibilidade de dados muito acessados quando a origem fica temporariamente indisponível.
Quando não usar cache em APIs
Antes de aplicar cache em uma API, alguns pontos precisam de avaliação. Nem todo dado é um bom candidato para ficar guardado em uma camada intermediária.
Se a informação muda com muita frequência, o cache vira uma fonte constante de dados desatualizados. Da mesma forma, se a informação precisa estar sempre 100% atualizada, qualquer atraso já representa um erro de negócio.
Outro sinal de alerta é a ausência de um gargalo real de custo ou performance. Nesse caso, o cache em APIs pode trazer mais complexidade do que benefício.
A Microsoft também recomenda cuidado com dados sensíveis, principalmente quando vários usuários ou aplicações compartilham o mesmo cache. Nesses casos, o mais seguro é sempre buscar a informação na fonte principal.
Existe ainda o cenário em que a maioria das requisições não encontra o dado em cache. Quando isso acontece, o custo de verificar e preencher o cache supera o benefício.
Na prática, listas compartilhadas, dados pouco voláteis e informações lidas com frequência costumam ser os melhores casos de uso. Catálogos, configurações, tabelas de referência e conteúdos editoriais se encaixam bem nesse perfil.
Modelo sem cache
Cliente → API → Banco de Dados
No modelo mais simples, não existe cache algum. Toda requisição feita pelo cliente passa pela API e chega diretamente ao banco de dados.
Esse modelo se destaca pela simplicidade e pela garantia de dados sempre atualizados. No entanto, ele apresenta alto custo de processamento, pouca escalabilidade e sobrecarga no banco.
Por esse motivo, ele atende bem apenas aplicações pequenas ou sistemas com baixo volume de acesso. Mesmo assim, é o ponto de partida natural de quase todo projeto, e não há nada de errado nisso.
Cache em memória: o cache local da API
Cliente → (API → Cache Local) → Banco de Dados
No cache local, a API passa a armazenar dados diretamente na memória do próprio processo. No primeiro acesso, ela consulta o banco e guarda o resultado.
Nas próximas requisições, a API reutiliza o dado sem acessar o banco novamente. Esse fluxo de buscar no cache e, em caso de ausência, carregar da origem recebe o nome de padrão Cache-Aside.
Esse modelo de cache em memória é simples de implementar, não tem custo de rede para consultar o cache e reduz drasticamente o uso do banco de dados. Em muitos cenários reais, ele já resolve grande parte dos problemas de performance.
Exemplo de cache local em APIs .NET
No ecossistema .NET, o cache local usa a interface IMemoryCache, que representa um cache armazenado na memória do servidor web. O exemplo abaixo mostra uma Minimal API que guarda a lista de filmes em destaque por cinco minutos.
using Microsoft.Extensions.Caching.Memory;
var builder = WebApplication.CreateBuilder(args);
// Registra o cache em memória no container de injeção de dependência
builder.Services.AddMemoryCache();
builder.Services.AddScoped<IMovieRepository, MovieRepository>();
var app = builder.Build();
app.MapGet("/movies/featured", async (IMemoryCache cache, IMovieRepository repository) =>
{
// Busca no cache e, se não encontrar, consulta o banco (Cache-Aside)
var movies = await cache.GetOrCreateAsync("movies:featured", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return await repository.GetFeaturedAsync();
});
return Results.Ok(movies);
});
app.Run();
O método GetOrCreateAsync concentra a lógica de leitura e preenchimento. Já a propriedade AbsoluteExpirationRelativeToNow define por quanto tempo o dado permanece válido.
Se quiser se aprofundar nessa interface, o artigo sobre cache em memória no ASP.NET detalha as opções de expiração e prioridade. Para cachear respostas HTTP inteiras, vale conhecer também o Output Cache em APIs .NET.
Limitações do cache local
A principal limitação está no fato de que o cache não é compartilhado entre processos. Além disso, ao reiniciar a aplicação, todo o conteúdo em memória desaparece.
Outro ponto de atenção é o consumo de memória. Como o cache vive dentro do processo da API, um volume grande de dados pode competir com o restante da aplicação por recursos.
Cache local em APIs com múltiplos servidores
Cliente → Load Balancer → (APIs → Cache Local) → Banco de Dados
Quando uma API cresce e passa a rodar em múltiplas instâncias atrás de um balanceador de carga, cada instância mantém seu próprio cache local. Assim, o mesmo dado pode existir em três, cinco ou dez cópias diferentes.
Nesse cenário, algumas requisições se beneficiam do cache, enquanto outras ainda precisam consultar o banco. Isso acontece porque a instância que recebe a requisição pode ainda não ter aquele dado em memória.
Embora esse modelo represente uma melhora significativa em relação a não usar cache, ele não elimina completamente as consultas repetidas. Também surge um problema de consistência, porque cada cópia expira em um momento diferente.
A documentação do ASP.NET Core recomenda sessões fixas, também chamadas de sticky sessions, quando a aplicação usa cache em memória em um conjunto de servidores. Com elas, o balanceador envia todas as requisições de um mesmo cliente para o mesmo servidor.
Essa saída funciona, mas cria dependência de afinidade entre cliente e servidor. Por isso, à medida que a aplicação escala, a alternativa mais comum passa a ser o cache distribuído.
Cache distribuído em APIs
Cliente → API → Cache Distribuído → Banco de Dados
No modelo de cache distribuído, o cache passa a ser um serviço separado da API, como o Redis. Todas as instâncias da aplicação compartilham o mesmo cache, o que garante maior consistência e reduz ainda mais o número de acessos ao banco de dados.
Esse tipo de cache em APIs é ideal para aplicações maiores, com alto volume de acesso ou múltiplos servidores. Além disso, o conteúdo sobrevive a reinícios e novas implantações da API, já que vive fora do processo.
Por outro lado, ele introduz infraestrutura adicional e aumenta a complexidade da arquitetura. Cada leitura agora envolve uma chamada de rede e a serialização do objeto.
Exemplo de cache distribuído com IDistributedCache
No .NET, o contrato para esse modelo é a interface IDistributedCache. O ASP.NET Core trabalha com implementações para SQL Server, Redis, PostgreSQL e NCache, entre outras.
using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;
// Registra o Redis como implementação de IDistributedCache
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
app.MapGet("/products", async (IDistributedCache cache, IProductRepository repository) =>
{
const string cacheKey = "products:all";
// O cache distribuído trabalha com bytes ou strings, então é preciso serializar
var cached = await cache.GetStringAsync(cacheKey);
if (cached is not null)
return Results.Ok(JsonSerializer.Deserialize<List<Product>>(cached));
var products = await repository.GetAllAsync();
await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(products),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
return Results.Ok(products);
});
Repare que o código ficou mais longo, porque a interface armazena apenas byte[] ou strings. Dessa forma, a serialização e a desserialização passam a ser responsabilidade da aplicação.
Quem já usa PostgreSQL e não quer adicionar o Redis à infraestrutura pode avaliar o artigo sobre PostgreSQL como cache distribuído.
Cache híbrido: local e distribuído juntos
Cliente → (API → Cache Local) → Cache Distribuído → Banco de Dados
O cache híbrido combina cache local e cache distribuído em uma única estratégia. Primeiro, a API verifica se o dado está disponível em memória.
Caso não esteja, ela consulta o cache distribuído. Apenas se o dado não existir em nenhum dos dois, a API acessa o banco de dados e, em seguida, preenche as duas camadas.
Esse modelo oferece o melhor equilíbrio entre performance e consistência. O banco recebe o mínimo possível de consultas, enquanto o cache local garante respostas rápidas.
O trade-off está na complexidade: mais código, mais infraestrutura e maior custo de manutenção. Além disso, invalidar um dado exige atenção, porque ele pode estar em várias memórias locais ao mesmo tempo.
HybridCache: cache em APIs com duas camadas no .NET
A partir do .NET 9, a biblioteca HybridCache entrega esse modelo pronto. Ela gerencia automaticamente a camada em memória, chamada de L1, e a camada distribuída, chamada de L2.
using Microsoft.Extensions.Caching.Hybrid;
// O HybridCache usa o IDistributedCache registrado como segunda camada
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(30),
LocalCacheExpiration = TimeSpan.FromMinutes(5)
};
});
app.MapGet("/catalog/{category}", async (string category, HybridCache cache,
ICatalogRepository repository, CancellationToken token) =>
{
// Procura na memória, depois no Redis e só então no banco
var items = await cache.GetOrCreateAsync(
$"catalog:{category}",
async cancel => await repository.GetByCategoryAsync(category, cancel),
tags: ["catalog"],
cancellationToken: token);
return Results.Ok(items);
});
Além das duas camadas, o HybridCache oferece proteção contra cache stampede. Quando muitas requisições pedem a mesma chave ao mesmo tempo, apenas uma executa a consulta, enquanto as outras aguardam o resultado.
A biblioteca também traz serialização configurável e invalidação por tags. Contudo, a documentação faz uma ressalva: a invalidação afeta o servidor atual e o cache distribuído, mas não alcança a memória local das outras instâncias.
Expiração e consistência no cache em APIs
Qualquer modelo de cache em APIs guarda uma cópia, e toda cópia pode ficar desatualizada. Por isso, a política de expiração é tão importante quanto a escolha do tipo de cache.
Um tempo de expiração muito curto faz a aplicação buscar o dado no banco o tempo todo. Já um tempo muito longo entrega informações velhas ao usuário.
Para dados que a própria aplicação altera, existem duas abordagens comuns. Na primeira, a API grava no banco e remove a entrada do cache, para que a próxima leitura busque o valor novo.
Na segunda, chamada de write-through, a API atualiza o banco e o cache na mesma operação. Com isso, o leitor recebe o valor novo imediatamente, ao custo de uma escrita mais lenta e mais coordenação.
Também vale planejar o que acontece quando o cache falha. A recomendação das diretrizes de arquitetura é não devolver erro ao cliente, mas buscar o dado na origem, com cuidado para não sobrecarregar o banco nesse momento.
A evolução natural do cache em APIs
Na prática, o cache em APIs raramente nasce no modelo mais complexo. O caminho natural costuma ser evolutivo.
Primeiro, a aplicação começa sem cache. Em seguida, ganha um cache local quando surge o primeiro gargalo de leitura.
Depois, quando a API passa a rodar em várias instâncias, a equipe evolui para o cache distribuído. Por fim, apenas quando necessário, aplica o modelo híbrido.
Cada etapa resolve um problema específico. Portanto, adote cada uma conforme a maturidade e a necessidade da aplicação, e não por antecipação.
Uma boa prática é medir antes de decidir. Métricas de tempo de resposta, consumo do banco e taxa de acerto do cache mostram se a camada adicional realmente está pagando o seu custo.
Conclusão
Cache é uma decisão técnica e também estratégica. Nem toda API precisa de cache distribuído ou híbrido, mas praticamente toda aplicação moderna precisa, em algum momento, considerar o uso de cache.
O modelo sem cache oferece simplicidade e dados sempre atuais. Já o cache local entrega um ganho enorme com pouco esforço, enquanto o distribuído resolve a consistência entre instâncias.
O cache híbrido, por sua vez, combina o melhor dos dois mundos ao custo de mais complexidade. No .NET, ferramentas como IMemoryCache, IDistributedCache e HybridCache cobrem todos esses cenários.
Compreender os tipos de cache em APIs, saber quando usá-los e entender seus trade-offs é essencial. Assim, você constrói sistemas mais performáticos, escaláveis e sustentáveis ao longo do tempo.
FAQ
O que é cache em APIs?
É uma camada que guarda cópias de dados já consultados para responder às próximas requisições sem acessar o banco. Com isso, a API ganha velocidade e o banco recebe menos carga.
Qual a diferença entre cache local e cache distribuído?
O cache local vive na memória de cada instância da API e se perde quando ela reinicia. O distribuído fica em um serviço externo, como o Redis, e é compartilhado por todas as instâncias.
Quando devo usar cache híbrido?
Quando a API roda em várias instâncias, precisa de respostas muito rápidas e quer reduzir chamadas de rede ao cache distribuído. Fora desse cenário, um modelo mais simples costuma bastar.
O HybridCache substitui o IMemoryCache e o IDistributedCache?
Ele foi pensado como substituto para os dois casos de uso, oferecendo uma API unificada. Ainda assim, aplicações simples de um único servidor podem continuar usando apenas o IMemoryCache.
Todo dado pode ficar em cache?
Não. Dados muito voláteis, que exigem consistência total ou que são sensíveis e compartilhados entre usuários devem vir sempre da fonte principal.
O que é cache stampede?
É quando uma entrada muito usada expira e muitas requisições tentam recarregá-la ao mesmo tempo, sobrecarregando o banco. O HybridCache evita isso deixando apenas uma requisição executar a consulta.