Caching
Implemente estratégias de cache para máxima performance e redução de custos
Este guia apresenta estratégias de caching para otimizar performance e reduzir custos da API RenderHub.
Por Que Cachear?
Benefícios do Caching
- Performance: Reduz latência de segundos para milissegundos
- Custos: Evita chamadas desnecessárias à API
- Escalabilidade: Reduz carga nos servidores
- Disponibilidade: Continua servindo em caso de falha da API
Quando Cachear?
Cache é ideal quando:
- ✅ Dados mudam raramente (templates, configurações)
- ✅ Mesmo PDF é requisitado múltiplas vezes
- ✅ Alta taxa de leitura vs escrita
- ✅ Tolerância a dados levemente desatualizados
Não cache quando:
- ❌ Dados altamente dinâmicos (preços em tempo real)
- ❌ Dados sensíveis que mudam frequentemente
- ❌ PDFs únicos que nunca são requisitados novamente
Estratégias de Cache
1. Cache em Memória (In-Memory)
Rápido mas volátil. Ideal para dados frequentemente acessados.
import NodeCache from 'node-cache';
// ✅ Cache simples em memória
const pdfCache = new NodeCache({
stdTTL: 3600, // 1 hora
checkperiod: 600, // Limpar a cada 10 minutos
useClones: false, // Não clonar buffers (performance)
});
async function generatePDFWithCache(
templateId: string,
data: any
): Promise<Buffer> {
const cacheKey = generateCacheKey(templateId, data);
// Tentar recuperar do cache
const cached = pdfCache.get<Buffer>(cacheKey);
if (cached) {
logger.info('PDF cache hit', { cacheKey });
return cached;
}
// Cache miss - gerar PDF
logger.info('PDF cache miss', { cacheKey });
const pdf = await renderHub.generatePDF(templateId, data);
// Armazenar no cache
pdfCache.set(cacheKey, pdf);
return pdf;
}
// Gerar chave de cache baseada em dados
function generateCacheKey(templateId: string, data: any): string {
const dataHash = crypto
.createHash('sha256')
.update(JSON.stringify(data))
.digest('hex');
return `pdf:${templateId}:${dataHash}`;
}
Vantagens:
- Muito rápido (< 1ms)
- Simples de implementar
- Sem dependências externas
Desvantagens:
- Perdido ao reiniciar aplicação
- Limitado à memória RAM
- Não compartilhado entre instâncias
2. Cache Distribuído (Redis)
Persistente e compartilhado. Ideal para múltiplas instâncias.
import Redis from 'ioredis';
const redis = new Redis({
host: process.env.REDIS_HOST,
port: 6379,
password: process.env.REDIS_PASSWORD,
maxRetriesPerRequest: 3,
});
// ✅ Cache distribuído com Redis
async function generatePDFWithRedisCache(
templateId: string,
data: any
): Promise<Buffer> {
const cacheKey = generateCacheKey(templateId, data);
// Tentar recuperar do cache
const cached = await redis.getBuffer(cacheKey);
if (cached) {
logger.info('Redis cache hit', { cacheKey });
return cached;
}
// Cache miss - gerar PDF
logger.info('Redis cache miss', { cacheKey });
const pdf = await renderHub.generatePDF(templateId, data);
// Armazenar no cache (expiração: 1 hora)
await redis.setex(cacheKey, 3600, pdf);
return pdf;
}
Vantagens:
- Persistente (sobrevive a reinicializações)
- Compartilhado entre instâncias
- Suporta TTL automático
- Muito rápido (1-5ms)
Desvantagens:
- Requer infraestrutura adicional
- Custo de rede (pequeno)
- Complexidade adicional
3. Cache em Disco (File System)
Persistente e ilimitado. Ideal para PDFs grandes ou muitos PDFs.
import { promises as fs } from 'fs';
import path from 'path';
import crypto from 'crypto';
const CACHE_DIR = path.join(__dirname, '../cache/pdfs');
// Criar diretório de cache
await fs.mkdir(CACHE_DIR, { recursive: true });
// ✅ Cache em disco
async function generatePDFWithFileCache(
templateId: string,
data: any
): Promise<Buffer> {
const cacheKey = generateCacheKey(templateId, data);
const cachePath = path.join(CACHE_DIR, `${cacheKey}.pdf`);
// Tentar recuperar do cache
try {
const stats = await fs.stat(cachePath);
const age = Date.now() - stats.mtimeMs;
// Verificar se não expirou (1 hora)
if (age < 3600000) {
logger.info('File cache hit', { cacheKey });
return await fs.readFile(cachePath);
} else {
// Expirado - deletar
await fs.unlink(cachePath);
}
} catch (error) {
// Arquivo não existe
}
// Cache miss - gerar PDF
logger.info('File cache miss', { cacheKey });
const pdf = await renderHub.generatePDF(templateId, data);
// Armazenar no cache
await fs.writeFile(cachePath, pdf);
return pdf;
}
// Job de limpeza de cache antigo
import cron from 'node-cron';
cron.schedule('0 * * * *', async () => {
const files = await fs.readdir(CACHE_DIR);
const now = Date.now();
let deleted = 0;
for (const file of files) {
const filePath = path.join(CACHE_DIR, file);
const stats = await fs.stat(filePath);
const age = now - stats.mtimeMs;
// Deletar se mais de 1 hora
if (age > 3600000) {
await fs.unlink(filePath);
deleted++;
}
}
logger.info('Cache cleanup completed', { deleted });
});
Vantagens:
- Persistente
- Sem limite de tamanho (exceto disco)
- Sem dependências externas
- Barato
Desvantagens:
- Mais lento que memória (10-50ms)
- Requer gerenciamento de limpeza
- I/O pode ser gargalo
4. Cache em Object Storage (S3)
Escalável e durável. Ideal para produção.
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });
const CACHE_BUCKET = 'renderhub-pdf-cache';
// ✅ Cache em S3
async function generatePDFWithS3Cache(
templateId: string,
data: any
): Promise<Buffer> {
const cacheKey = generateCacheKey(templateId, data);
const s3Key = `cache/${cacheKey}.pdf`;
// Tentar recuperar do cache
try {
const response = await s3.send(
new GetObjectCommand({
Bucket: CACHE_BUCKET,
Key: s3Key,
IfModifiedSince: new Date(Date.now() - 3600000), // 1 hora
})
);
const pdf = await response.Body?.transformToByteArray();
if (pdf) {
logger.info('S3 cache hit', { cacheKey });
return Buffer.from(pdf);
}
} catch (error) {
// Não encontrado ou expirado
}
// Cache miss - gerar PDF
logger.info('S3 cache miss', { cacheKey });
const pdf = await renderHub.generatePDF(templateId, data);
// Armazenar no cache
await s3.send(
new PutObjectCommand({
Bucket: CACHE_BUCKET,
Key: s3Key,
Body: pdf,
ContentType: 'application/pdf',
CacheControl: 'max-age=3600', // 1 hora
})
);
return pdf;
}
// Configurar lifecycle policy no bucket para deletar objetos antigos
// Lifecycle rule (via AWS Console ou Terraform):
// {
// "Rules": [{
// "Id": "DeleteOldCache",
// "Status": "Enabled",
// "Expiration": { "Days": 7 }
// }]
// }
Vantagens:
- Altamente escalável
- Durável (99.999999999%)
- Sem gerenciamento de servidor
- Lifecycle policies automáticas
Desvantagens:
- Latência maior (50-200ms)
- Custo por requisição e storage
- Complexidade de configuração
Cache Híbrido (Multi-Layer)
Combine múltiplas estratégias para melhor performance:
// ✅ Cache em 3 camadas: Memória → Redis → S3
class MultiLayerCache {
private memoryCache: NodeCache;
private redis: Redis;
private s3: S3Client;
constructor() {
this.memoryCache = new NodeCache({ stdTTL: 300 }); // 5 min
this.redis = new Redis();
this.s3 = new S3Client({ region: 'us-east-1' });
}
async get(key: string): Promise<Buffer | null> {
// Layer 1: Memória (< 1ms)
const fromMemory = this.memoryCache.get<Buffer>(key);
if (fromMemory) {
logger.debug('Memory cache hit', { key });
return fromMemory;
}
// Layer 2: Redis (1-5ms)
const fromRedis = await this.redis.getBuffer(key);
if (fromRedis) {
logger.debug('Redis cache hit', { key });
// Popular memória para próximas requisições
this.memoryCache.set(key, fromRedis);
return fromRedis;
}
// Layer 3: S3 (50-200ms)
try {
const response = await this.s3.send(
new GetObjectCommand({
Bucket: 'renderhub-pdf-cache',
Key: `cache/${key}.pdf`,
})
);
const pdf = await response.Body?.transformToByteArray();
if (pdf) {
const buffer = Buffer.from(pdf);
logger.debug('S3 cache hit', { key });
// Popular camadas superiores
this.memoryCache.set(key, buffer);
await this.redis.setex(key, 3600, buffer);
return buffer;
}
} catch (error) {
// Não encontrado
}
return null;
}
async set(key: string, value: Buffer): Promise<void> {
// Armazenar em todas as camadas
this.memoryCache.set(key, value);
await this.redis.setex(key, 3600, value);
await this.s3.send(
new PutObjectCommand({
Bucket: 'renderhub-pdf-cache',
Key: `cache/${key}.pdf`,
Body: value,
})
);
}
async delete(key: string): Promise<void> {
// Invalidar em todas as camadas
this.memoryCache.del(key);
await this.redis.del(key);
await this.s3.send(
new DeleteObjectCommand({
Bucket: 'renderhub-pdf-cache',
Key: `cache/${key}.pdf`,
})
);
}
}
// Uso
const cache = new MultiLayerCache();
async function generatePDF(templateId: string, data: any): Promise<Buffer> {
const cacheKey = generateCacheKey(templateId, data);
// Tentar cache
const cached = await cache.get(cacheKey);
if (cached) {
return cached;
}
// Cache miss - gerar PDF
const pdf = await renderHub.generatePDF(templateId, data);
// Armazenar em cache
await cache.set(cacheKey, pdf);
return pdf;
}
Invalidação de Cache
Invalidação por TTL (Time To Live)
Método mais simples - cache expira automaticamente:
// ✅ TTL automático
await redis.setex(cacheKey, 3600, pdf); // Expira em 1 hora
Invalidação Manual
Invalide cache quando dados mudarem:
// ✅ Invalidar ao atualizar dados
async function updateInvoice(invoiceId: string, updates: any) {
// Atualizar banco de dados
await db.invoices.update(updates, { where: { id: invoiceId } });
// Invalidar cache do PDF
const cacheKey = `pdf:invoice:${invoiceId}`;
await cache.delete(cacheKey);
logger.info('Cache invalidated', { invoiceId, cacheKey });
}
Invalidação por Tag
Invalide grupos de cache relacionados:
// ✅ Sistema de tags
class TaggedCache {
private redis: Redis;
async set(key: string, value: Buffer, tags: string[]): Promise<void> {
// Armazenar valor
await this.redis.setex(key, 3600, value);
// Associar tags
for (const tag of tags) {
await this.redis.sadd(`tag:${tag}`, key);
}
}
async invalidateTag(tag: string): Promise<void> {
// Obter todas as keys com essa tag
const keys = await this.redis.smembers(`tag:${tag}`);
if (keys.length > 0) {
// Deletar todas as keys
await this.redis.del(...keys);
// Deletar o set de tags
await this.redis.del(`tag:${tag}`);
logger.info('Cache tag invalidated', { tag, keysDeleted: keys.length });
}
}
}
// Uso
const cache = new TaggedCache();
// Armazenar com tags
await cache.set(
'pdf:invoice:123',
pdf,
['invoices', 'customer:456', 'template:invoice-v2']
);
// Invalidar todos os PDFs de um cliente
await cache.invalidateTag('customer:456');
// Invalidar todos os PDFs de um template
await cache.invalidateTag('template:invoice-v2');
Cache de Template IDs
Templates raramente mudam - cachear IDs:
// ✅ Cache de template IDs
class TemplateCache {
private cache = new Map<string, string>();
async getTemplateId(name: string): Promise<string> {
// Verificar cache
if (this.cache.has(name)) {
return this.cache.get(name)!;
}
// Cache miss - buscar da API
const template = await renderHub.getTemplate(name);
// Armazenar no cache (sem expiração)
this.cache.set(name, template.id);
return template.id;
}
invalidate(name: string): void {
this.cache.delete(name);
}
clear(): void {
this.cache.clear();
}
}
const templateCache = new TemplateCache();
// Uso
const templateId = await templateCache.getTemplateId('invoice');
const pdf = await renderHub.generatePDF(templateId, data);
Cache Condicional
Cache apenas quando faz sentido:
// ✅ Cache condicional baseado em tipo de documento
async function generatePDFWithConditionalCache(
templateId: string,
data: any,
options: { cacheable?: boolean } = {}
): Promise<Buffer> {
// Não cachear se explicitamente desabilitado
if (options.cacheable === false) {
return await renderHub.generatePDF(templateId, data);
}
// Heurística: não cachear se dados são muito dinâmicos
const isDynamic = data.timestamp || data.randomId || data.realTimePrice;
if (isDynamic) {
logger.debug('Skipping cache for dynamic data');
return await renderHub.generatePDF(templateId, data);
}
// Usar cache normalmente
return await generatePDFWithCache(templateId, data);
}
Pré-aquecimento de Cache (Cache Warming)
Gere PDFs populares antes de serem requisitados:
// ✅ Pré-aquecer cache com PDFs frequentes
async function warmCache() {
logger.info('Starting cache warming');
// PDFs mais requisitados nas últimas 24h
const popularPdfs = await db.pdfRequests.findAll({
where: {
createdAt: { [Op.gt]: new Date(Date.now() - 86400000) },
},
attributes: ['templateId', 'dataHash'],
group: ['templateId', 'dataHash'],
order: [[sequelize.fn('COUNT', '*'), 'DESC']],
limit: 100,
});
// Gerar e cachear
for (const { templateId, dataHash } of popularPdfs) {
const data = await getDataByHash(dataHash);
await generatePDFWithCache(templateId, data);
}
logger.info('Cache warming completed', { count: popularPdfs.length });
}
// Executar pré-aquecimento diariamente
import cron from 'node-cron';
cron.schedule('0 6 * * *', warmCache); // 6am diariamente
Monitoramento de Cache
Métricas de Cache
Monitore eficácia do cache:
import { Counter, Histogram } from 'prom-client';
const cacheHits = new Counter({
name: 'pdf_cache_hits_total',
help: 'Total de cache hits',
labelNames: ['layer'],
});
const cacheMisses = new Counter({
name: 'pdf_cache_misses_total',
help: 'Total de cache misses',
});
const cacheDuration = new Histogram({
name: 'pdf_cache_duration_seconds',
help: 'Duração de operações de cache',
labelNames: ['operation', 'layer'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
});
// Instrumentar cache
class InstrumentedCache {
async get(key: string): Promise<Buffer | null> {
const start = performance.now();
const value = await this.cache.get(key);
const duration = (performance.now() - start) / 1000;
cacheDuration.observe({ operation: 'get', layer: 'redis' }, duration);
if (value) {
cacheHits.inc({ layer: 'redis' });
} else {
cacheMisses.inc();
}
return value;
}
}
Dashboard de Cache
Calcule taxa de acerto (hit rate):
// Hit rate = hits / (hits + misses)
const hitRate = cacheHits / (cacheHits + cacheMisses);
// Meta: > 80% hit rate
if (hitRate < 0.8) {
logger.warn('Low cache hit rate', { hitRate });
}
Comparação de Estratégias
| Estratégia | Latência | Persistência | Compartilhado | Custo | Complexidade |
|---|---|---|---|---|---|
| Memória | < 1ms | ❌ | ❌ | Grátis | Baixa |
| Redis | 1-5ms | ✅ | ✅ | Baixo | Média |
| Disco | 10-50ms | ✅ | ❌ | Grátis | Média |
| S3 | 50-200ms | ✅ | ✅ | Médio | Alta |
| Multi-Layer | 1-200ms | ✅ | ✅ | Médio | Alta |
Checklist de Caching
- Estratégia de cache definida
- TTL apropriado configurado
- Chaves de cache únicas e determinísticas
- Invalidação implementada
- Limpeza de cache antigo (se file system)
- Cache de template IDs
- Pré-aquecimento para PDFs populares (opcional)
- Métricas de cache coletadas
- Hit rate monitorado (meta: > 80%)
- Alertas para hit rate baixo
- Cache warming para alta disponibilidade
- Testes de cache implementados
Próximos Passos
- Otimize com guia de Performance
- Implemente com Best Practices
- Proteja com guia de Security