Guides

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

  1. Performance: Reduz latência de segundos para milissegundos
  2. Custos: Evita chamadas desnecessárias à API
  3. Escalabilidade: Reduz carga nos servidores
  4. 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égiaLatênciaPersistênciaCompartilhadoCustoComplexidade
Memória< 1msGrátisBaixa
Redis1-5msBaixoMédia
Disco10-50msGrátisMédia
S350-200msMédioAlta
Multi-Layer1-200msMédioAlta

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

On this page