Padrões de Caching em Produção: Redis, CDN e Application-level
Estratégias multi-nível de caching podem reduzir latência, custos de infraestrutura e carga no banco. Aprenda padrões, trade-offs e práticas de implementação.
Resumo executivo
Estratégias multi-nível de caching podem reduzir latência, custos de infraestrutura e carga no banco. Aprenda padrões, trade-offs e práticas de implementação.
Ultima atualizacao: 14/03/2026
Introdução: Caching como arquitetura, não otimização
Caching é frequentemente tratado como otimização tardia — algo a adicionar quando o sistema fica lento. Em 2026, caching maduro é arquitetura de primeira classe. Sistemas em escala operam com múltiplas camadas de cache: CDN no edge, cache application-level em memória, Redis como cache distribuído e até cache de query no banco de dados.
A diferença entre sistemas que escalam e aqueles que colapsam sob carga não é apenas poder de processamento — é a capacidade de servir requisições da memória, não do disco ou da rede.
Cada camada de cache serve um propósito específico: CDN para conteúdo estático global, application cache para dados frequentes na mesma instância, Redis para compartilhamento entre instâncias e query cache para otimizar acesso ao banco.
Padrões fundamentais de caching
Cache-aside (Lazy loading)
O padrão mais comum, onde a aplicação carrega dados no cache sob demanda:
typescriptinterface CacheAsideService {
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttl?: number): Promise<void>;
delete(key: string): Promise<void>;
}
async function getUser(cache: CacheAsideService, db: Database, userId: string): Promise<User> {
// 1. Tentar obter do cache
const cached = await cache.get<User>(`user:${userId}`);
if (cached) {
return cached; // Cache hit
}
// 2. Cache miss: buscar do banco
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
// 3. Armazenar no cache para futuras requisições
await cache.set(`user:${userId}`, user, 3600); // TTL de 1 hora
return user;
}Vantagens:
- Apenas dados acessados são cacheados
- Implementação simples
- O cache é auto-populando
Desvantagens:
- Primeira requisição após cache invalidation sempre é um cache miss
- Cache stampede possível quando muitos clientes tentam popular o mesmo item simultaneamente
Solução para cache stampede:
typescriptasync function getUserWithLock(cache: CacheAsideService, db: Database, userId: string): Promise<User> {
const cached = await cache.get<User>(`user:${userId}`);
if (cached) return cached;
// Adquirir lock para evitar cache stampede
const lockKey = `lock:user:${userId}`;
const lockAcquired = await cache.setIfNotExists(lockKey, 'locked', 30);
if (lockAcquired) {
try {
// Apenas quem adquiriu o lock popula o cache
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
await cache.set(`user:${userId}`, user, 3600);
return user;
} finally {
await cache.delete(lockKey);
}
} else {
// Aguardar e tentar obter do cache novamente
await sleep(100);
return getUserWithLock(cache, db, userId);
}
}Write-through
Dados são escritos tanto no cache quanto no armazenamento persistente simultaneamente:
typescriptasync function updateUser(cache: CacheAsideService, db: Database, userId: string, data: Partial<User>): Promise<User> {
// Atualizar banco de dados
const updated = await db.query(
'UPDATE users SET ... WHERE id = $1 RETURNING *',
[...data, userId]
);
// Atualizar cache imediatamente
await cache.set(`user:${userId}`, updated, 3600);
return updated;
}Vantagens:
- Cache sempre consistente com o banco
- Cache hits subsequentes sempre retornam dados atualizados
- Simples de implementar
Desvantagens:
- Operações de escrita são mais lentas (duas escritas)
- Writes que falham no banco mas sucedem no cache causam inconsistência
When to use: Dados frequentemente lidos e ocasionalmente escritos (profiles, configs, lookup tables).
Write-behind
Dados são escritos no cache imediatamente e no banco de forma assíncrona:
typescriptclass WriteBehindService {
private writeQueue: Queue<WriteOperation>;
async write(key: string, value: any): Promise<void> {
// Escrita síncrona no cache
await this.cache.set(key, value);
// Enfileirar escrita no banco
await this.writeQueue.add({ key, value, timestamp: Date.now() });
}
constructor() {
// Processar fila de forma assíncrona
this.writeQueue.process(async (op) => {
await this.db.query('INSERT INTO data_store (key, value) VALUES ($1, $2)', [op.key, op.value]);
});
}
}Vantagens:
- Escritas são extremamente rápidas
- Alto throughput de escrita
- Batch de escritas pode ser consolidado
Desvantagens:
- Risco de perda de dados se o cache falhar antes de persistir
- Complexidade de implementação aumenta significativamente
- Necessário mecanismo de persistência durável
When to use: Logs, analytics, contadores, dados não-críticos que podem ser tolerados com perda temporária.
Estratégias de invalidação de cache
Time-based expiration (TTL)
A forma mais simples de invalidação:
typescript// Dados com diferentes perfis de atualização
const CACHE_TTLS = {
static: 86400, // 24h: conteúdo raramente muda
user: 3600, // 1h: perfis de usuário mudam ocasionalmente
session: 1800, // 30m: sessions expiram naturalmente
realtime: 60, // 1m: dados que precisam ser quase em tempo real
volatile: 10, // 10s: dados que mudam frequentemente
};Desvantagens: Dados desatualizados podem ser servidos até o TTL expirar.
Event-based invalidation
Invalidar cache quando dados mudam:
typescriptclass CacheInvalidator {
async invalidateUser(userId: string): Promise<void> {
// Invalidar cache direto
await this.cache.delete(`user:${userId}`);
// Invalidar caches derivados
await this.cache.deletePattern(`user:${userId}:*`);
// Emitir evento para outros serviços
await this.eventBus.publish('user.invalidated', { userId });
}
}Desafio: Ordem de operações deve ser consistente:
typescript// ERRADO: Cache pode ser populado com dados antigos
async function updateUserData(userId: string, data: any) {
await this.cache.invalidate(userId);
await this.db.update(userId, data);
}
// CORRETO: Primeiro persiste, depois invalida
async function updateUserData(userId: string, data: any) {
await this.db.update(userId, data);
await this.cache.invalidate(userId);
}Version-based caching
Usar versões para evitar race conditions:
typescriptinterface VersionedCache {
get<T>(key: string, version: number): Promise<T | null>;
set<T>(key: string, version: number, value: T): Promise<void>;
incrementVersion(key: string): Promise<number>;
}
async function getUser(cache: VersionedCache, db: Database, userId: string): Promise<User> {
const currentVersion = await cache.getCurrentVersion(`user:${userId}`);
const cached = await cache.get(`user:${userId}`, currentVersion);
if (cached) return cached;
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
await cache.set(`user:${userId}`, currentVersion, user);
return user;
}
async function invalidateUser(cache: VersionedCache, userId: string): Promise<void> {
await cache.incrementVersion(`user:${userId}`);
}Arquitetura multi-nível de caching
Nível 1: CDN (Content Delivery Network)
Para conteúdo estático e semi-estático:
┌─────────────────────────────────────────────────────────────────────────┐
│ CDN LAYER │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Browser → Edge Location (CDN) → Origin │
│ │
│ Cached: CSS, JS, Images, Fonts, API Responses │
│ TTL: 1h - 24h (configurável por resource) │
│ Invalidation: Manual purge or versioned URLs │
│ │
└─────────────────────────────────────────────────────────────────────────┘Práticas:
- Versionar URLs de assets (
app.v2.jsem vez deapp.js) - Configurar cache headers apropriados
- Usar cache key purging estratégico para atualizações urgentes
nginx# Nginx config para cache de API responses
location /api/public/ {
proxy_pass http://backend;
proxy_cache api_cache;
proxy_cache_valid 200 60m; # Cache respostas 200 por 60 minutos
proxy_cache_bypass $http_cache_control; # Respeitar no-cache do cliente
add_header X-Cache-Status $upstream_cache_status;
}Nível 2: Application-level cache (in-memory)
Cache local na mesma instância da aplicação:
typescriptclass ApplicationCache {
private cache = new Map<string, { value: any; expires: number }>();
get<T>(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expires) {
this.cache.delete(key);
return null;
}
return entry.value as T;
}
set<T>(key: string, value: T, ttlSeconds: number): void {
this.cache.set(key, {
value,
expires: Date.now() + ttlSeconds * 1000,
});
}
delete(key: string): void {
this.cache.delete(key);
}
}Vantagens:
- Extremamente rápido (sem latência de rede)
- Não requer infraestrutura adicional
- Ideal para dados acessados frequentemente na mesma instância
Desvantagens:
- Dados não são compartilhados entre instâncias
- Cada instância popula seu cache, desperdiçando recursos
- Cache não persiste entre restarts
When to use: Configurações, lookup tables pequenas, resultados computacionalmente caros.
Nível 3: Distributed cache (Redis)
Cache compartilhado entre todas as instâncias:
typescriptimport { Redis } from 'ioredis';
class RedisCache {
constructor(private redis: Redis) {}
async get<T>(key: string): Promise<T | null> {
const value = await this.redis.get(key);
return value ? JSON.parse(value) : null;
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
const serialized = JSON.stringify(value);
if (ttl) {
await this.redis.setex(key, ttl, serialized);
} else {
await this.redis.set(key, serialized);
}
}
async delete(key: string): Promise<void> {
await this.redis.del(key);
}
async deletePattern(pattern: string): Promise<void> {
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
}
}
}Vantagens:
- Cache compartilhado entre instâncias
- Persistência configurável
- Suporte a estruturas de dados avançadas (sets, sorted sets, streams)
- Pub/sub para invalidação distribuída
Desvantagens:
- Latência de rede adicionada
- SPOF se não configurado com alta disponibilidade
- Custo operacional adicional
When to use: Dados frequentemente acessados, perfis de usuário, sessões, resultados de queries caras.
Nível 4: Database query cache
Cache de queries no próprio banco de dados:
sql-- PostgreSQL query cache
SET work_mem = '256MB';
SET shared_buffers = '2GB';
SET effective_cache_size = '8GB';
-- Materialized views para queries complexas
CREATE MATERIALIZED VIEW user_stats AS
SELECT user_id, COUNT(*) as total_orders, SUM(amount) as total_spent
FROM orders
GROUP BY user_id;
-- Refresh em intervalos regulares
REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats;Padrões avançados de caching
Read-through cache
Cache que popula automaticamente em miss:
typescriptclass ReadThroughCache {
constructor(
private cache: RedisCache,
private loader: (key: string) => Promise<any>
) {}
async get(key: string, ttl: number = 3600): Promise<any> {
let value = await this.cache.get(key);
if (!value) {
value = await this.loader(key);
await this.cache.set(key, value, ttl);
}
return value;
}
}
const userCache = new ReadThroughCache(redisCache, async (userId) => {
return await db.query('SELECT * FROM users WHERE id = $1', [userId]);
});Refresh-ahead (proactive loading)
Refresh cache antes de expirar:
typescriptclass RefreshAheadCache {
async get(key: string, ttl: number): Promise<any> {
let value = await this.cache.get(key);
if (!value) {
// Cache miss: carregar do loader
value = await this.loader(key);
await this.cache.set(key, value, ttl);
} else {
// Cache hit: verificar se está próximo da expiração
const ttlRemaining = await this.cache.ttl(key);
const refreshThreshold = ttl * 0.9; // Refresh quando 90% do TTL passou
if (ttlRemaining < refreshThreshold) {
// Refresh assíncrono
this.refreshInBackground(key);
}
}
return value;
}
private async refreshInBackground(key: string): Promise<void> {
const value = await this.loader(key);
await this.cache.set(key, value, this.ttl);
}
}Cache warming
Preencher cache proativamente durante startup ou em horários de baixa demanda:
typescriptclass CacheWarmer {
async warmUserCache(userIds: string[]): Promise<void> {
for (const userId of userIds) {
const user = await this.db.query('SELECT * FROM users WHERE id = $1', [userId]);
await this.cache.set(`user:${userId}`, user, 3600);
}
}
// Executar durante startup da aplicação
async onStartup(): Promise<void> {
const activeUsers = await this.db.query(
'SELECT id FROM users WHERE last_active > NOW() - INTERVAL 7 days'
);
await this.warmUserCache(activeUsers.map(u => u.id));
}
}Métricas e monitoramento
Métricas essenciais
typescriptinterface CacheMetrics {
// Taxas de hit/miss
hitRate: number; // cache_hits / (cache_hits + cache_misses)
missRate: number; // cache_misses / (cache_hits + cache_misses)
// Latência
avgHitLatency: number; // Tempo médio de resposta em cache hit
avgMissLatency: number; // Tempo médio de resposta em cache miss
// Evolução e tamanho
evictions: number; // Quantos itens foram evictados por TTL ou memória
memoryUsage: number; // Memória usada pelo cache
itemCount: number; // Número de itens em cache
}Implementação de métricas
typescriptclass InstrumentedCache {
private metrics = {
hits: 0,
misses: 0,
evictions: 0,
};
async get<T>(key: string): Promise<T | null> {
const value = await this.cache.get<T>(key);
if (value) {
this.metrics.hits++;
this.metrics.histogram('cache.hit.latency', latency);
} else {
this.metrics.misses++;
this.metrics.histogram('cache.miss.latency', latency);
}
return value;
}
getMetrics(): CacheMetrics {
const total = this.metrics.hits + this.metrics.misses;
return {
hitRate: this.metrics.hits / total,
missRate: this.metrics.misses / total,
evictions: this.metrics.evictions,
// ...
};
}
}Targets de produção:
- Hit rate > 80% para dados frequentemente acessados
- Latência de cache hit < 5ms
- Evolução por TTL > 90% (evitar evolução por memória insuficiente)
Plano de implementação em 30 dias
Semana 1: Identificar oportunidades de caching
- Mapear endpoints mais acessados
- Identificar queries mais lentas
- Classificar dados por perfil de atualização
Semana 2: Implementar cache-aside em endpoints críticos
- Adicionar Redis como cache distribuído
- Implementar application cache para dados locais
- Configurar CDN para conteúdo estático
Semana 3: Refinar estratégias de invalidação
- Implementar invalidação baseada em eventos
- Adicionar versioning para evitar race conditions
- Configurar cache warming para dados críticos
Semana 4: Monitorar e otimizar
- Implementar métricas de cache
- Ajustar TTLs baseado em padrões de acesso
- Ajustar tamanho de cache baseado em utilização
Sua aplicação sofre com latência ou custos elevados de infraestrutura? Fale com especialistas da Imperialis sobre estratégias de caching multi-nível, arquitetura de performance e otimização de custos em escala.