Corte 30% da memória do Redis: um playbook de CTO para enxugar o cache

Por Diogo Hudson Dias
CTO analyzing a Redis memory usage dashboard on a laptop next to server racks in a data center.

A Cloudflare acabou de dizer o que ninguém admite em voz alta: economizou 100 terabytes de memória otimizando o cache de DNS do 1.1.1.1. Não foi comprando instâncias novas. Foi eliminando desperdício. Se o cache deles conseguiu perder tanta gordura, o que você acha que o seu cluster de Redis ou Valkey está escondendo?

Eis a verdade incômoda: na maioria das stacks SaaS que auditamos, o Redis gasta mais memória com overhead do que com os seus dados de fato. O vilão é um coquetel de chaves superdimensionadas, payloads JSON, TTLs preguiçosos e políticas de remoção que acumulam entradas frias. A boa notícia é que dá para cortar 30–50% de memória em 90 dias sem prejudicar latência ou taxa de acerto—se você tratar o design do cache como um produto de primeira classe, não como uma camada de conveniência.

Por que seu cache está acima do peso (e por que ele custa mais do que você imagina)

Redis e Valkey são rapidíssimos, mas não fazem milagre. Em um sistema 64-bit, cada par chave/valor pequeno carrega um overhead estrutural que eclipsa o payload. Números de ordem de grandeza por chave:

  • Entrada de dicionário: ~24 bytes (ponteiros para chave, valor, próximo).
  • Cabeçalho(s) do objeto: ~16–24 bytes dependendo do tipo/codificação.
  • Cabeçalho SDS + folga do alocador para chave e valor: ~16–40 bytes no total (varia por classe de tamanho).
  • Contabilidade de TTL (se definido): outra entrada + metadados, facilmente ~24–40 bytes.
  • Overhead do alocador e fragmentação: mais 10–30% por cima.

Esses são números direcionais, não constantes de laboratório. Mas o ponto permanece: aquela chave fofinha de 20 bytes e valor de 8 bytes pode ocupar 100–200+ bytes na memória quando você considera estruturas e arredondamento do alocador. Com dezenas ou centenas de milhões de chaves, o desperdício é enorme.

Agora some os pecados de alto nível:

  • Objetos JSON, com chaves entre aspas e nomes de campos redundantes, inflacionam os valores em 2–4x em comparação com codificações binárias.
  • Chaves compostas que repetem strings longas de tenant ou namespace aumentam a duplicação em todo o conjunto de dados.
  • TTL definida para “quase para sempre” para “garantir” faz chaves frias ficarem ocupando espaço indefinidamente.
  • Políticas de remoção padrão como allkeys-lru ou random não protegem o conjunto realmente quente.

Nada disso aparece nos dashboards de “hit ratio está ok”. Aparece na sua fatura da AWS e nos seus gráficos de autoscaling.

Um framework de decisão para CTO cortar 30%+ em 90 dias

Etapa 1: Faça inventário e baseline de forma adulta

Você não otimiza o que não mede. Na primeira semana, responda a quatro perguntas com evidências, não com achismos:

  • Quais são os 10 principais prefixos de chave por memória? Use MEMORY USAGE por amostragem, redis-cli --memkeys ou um amostrador baseado em SCAN em uma hora canário. Em clouds gerenciadas, exporte as métricas keyspace e bytes se disponíveis.
  • Quão “pequenos” são os seus valores pequenos? Calcule p50/p90/p99 de tamanhos de chaves e valores por prefixo. Procure o antipadrão em que o valor mediano é <64 bytes e o overhead domina.
  • Qual é o seu conjunto quente vs frio? Acompanhe chaves únicas tocadas por minuto e sua distribuição de interchegada. Se 90% do tráfego atinge 5–10% das chaves, você pode ser agressivo com remoção e near-caches.
  • Onde está escondida a duplicação? Estime a sobreposição de Jaccard de substrings nas chaves (IDs de tenant, namespaces) e campos nos valores (strings repetidas). Repetição alta pede interning de strings ou compressão por dicionário.

Congele um baseline: bytes totais em uso, bytes por prefixo principal, taxa de acerto, latência p95 e a distribuição do tempo de vida das chaves (do set ao último hit). Esses viram seus critérios de aceite.

Etapa 2: Elimine a duplicação de chaves com normalização e interning

A maioria das chaves compostas carrega a mesma bagagem um milhão de vezes. Exemplo: tenant:abc123:region:us-east-1:resource:xyz. Substitua substrings repetidas por IDs compactos:

  • Atribua IDs inteiros a tenants, regiões e outros namespaces repetidos. Mantenha um dicionário pequeno e autoritativo no cache (ou no seu banco de dados primário) mapeando ID → string.
  • Reordene as partes da chave da mais para a menos variante (por exemplo, t:123|r:1|xyz). Isso melhora a distribuição da hashtable e ajuda a identificar oportunidades de sharding.
  • Se você precisar manter chaves legíveis para operações, adicione um serviço sidecar keymap para debugging que expanda IDs—não sobrecarregue cada chave em produção com strings completas.

Espere economias de 10–30% apenas nas chaves em sistemas multi-tenant. Sim, exige uma migração. Sim, vale a pena.

Etapa 3: Pare de fazer cache de JSON a menos que seja absolutamente necessário

JSON é ótimo para logs e humanos; é péssimo para memória quente. Migre para um esquema binário compacto e estável:

  • MessagePack se quiser algo fácil, multiplataforma, com pouca dor de cabeça.
  • Protocol Buffers se quiser esquemas explícitos, compatibilidade retro/forward e os menores tamanhos para objetos com muitos escalares.
  • FlatBuffers/Cap’n Proto se acesso zero-copy ou orçamentos de CPU extremamente apertados importarem.

Números reais: em uma plataforma de e-commerce que ajudamos, objetos de sessão tinham média de 680 bytes em JSON. Proto reduziu para 210 bytes sem mudar campos. A latência também melhorou porque o parsing ficou mais barato.

Dica para Redis/Valkey especificamente: agrupe múltiplos campos relacionados em um hash que permaneça dentro da codificação compacta listpack (campos pequenos, poucas entradas). Isso colapsa o overhead por campo em uma única alocação e pode superar uma única string binária para structs pequenas—teste ambos com seus dados.

Etapa 4: Comprima seletivamente onde faz diferença

Se você faz cache de payloads ricos em texto ou embeddings, compressão compensa—mas não para tudo. Uma política prática:

  • Só comprima valores >= 512–1.024 bytes. Abaixo disso, o overhead do alocador e o custo de CPU muitas vezes anulam o ganho.
  • Use zstd em nível baixo (1–3). Para esquemas repetidos (descrições de produto, prompts), alimente um dicionário específico do domínio para mais 10–20% de ganho.
  • Comprima no aplicativo antes do set, assim você controla o trade-off e mantém os conjuntos quentes descomprimidos.

Acompanhe o tempo de descompressão no mesmo histograma da latência de hit do cache. Se o p95 passar do seu SLO, recue ou aumente o limiar de corte.

Etapa 5: Ajuste TTLs ao tamanho certo com dados, não com medo

TTL é o seu único coletor de lixo garantido. Use-o. Eis um caminho sóbrio que quase ninguém segue:

  • Calcule a distribuição do tempo até o último hit por tipo de chave. Se 85% das chaves nunca são tocadas após 10 minutos, um TTL de 60 minutos é autossabotagem.
  • Aplique TTLs diferenciados por prefixo com base no valor de negócio e no custo de refresh. O prefixo caro e quente ganha TTL maior; prefixos baratos e raramente acessados ganham minutos, não horas.
  • Adicione decaimento de TTL para chaves com baixo desempenho: em um miss após remoção, defina um TTL menor, a menos que o uso se recupere. Isso evita reaquecer o conjunto frio indefinidamente.

Na prática, disciplina de TTL sozinha costuma render 10–20% de alívio de memória em uma semana, sem mudanças de código além de configurar setex/pipelining.

Etapa 6: Escolha uma política de remoção que proteja o conjunto quente

A maioria dos clusters roda silenciosamente allkeys-lru ou, pior, noeviction. Se o acesso segue uma distribuição enviesada (segue), LFU ganha de LRU para reter chaves quentes durante picos. Migre para allkeys-lfu em caches gerais e volatile-lfu quando você depende de TTL para a vida do objeto.

Fique de olho em ghost entries—chaves que giram rapidamente e expulsam itens quentes estáveis. Se vir isso, adicione um near-cache no app (Etapa 7) ou uma política de admissão (não admitir no primeiro acesso) para filtrar one-hit wonders.

Etapa 7: Adicione um near-cache na aplicação para filtrar duplicidades

Um cache em processo de 50–200 MB por instância de app pode blindar seu cache central contra duplicidades em rajadas e reduzir a latência ponta a ponta:

  • Use Caffeine (JVM) ou Ristretto (Go) com admissão TinyLFU e remoção W-TinyLFU. Essas políticas reduzem drasticamente a poluição do cache por chaves de uso único.
  • Defina um TTL curto (30–120s) e dimensione para caber confortavelmente em L3. O objetivo é interceptar surtos, não guardar tudo.
  • Registre uma métrica: Redis reads avoided. Se você não vir 20–40% de redução em leituras no backend no pico, ajuste admissão e tamanho.

Essa única etapa normalmente reduz 10–25% do footprint do cache central, permitindo encurtar TTLs centrais e usar LFU de forma mais agressiva.

Etapa 8: Faça o tuning do motor que você já tem

Depois de mudar o modelo de dados, esprema o alocador e as estruturas:

  • Ative a defragmentação ativa no Redis/Valkey para clusters de longa duração com churn. Ela não corrige erros de design, mas recupera fragmentação ao longo do tempo.
  • Avalie o tuning do jemalloc quando disponível: threads em background e limpeza de páginas sujas reduzem picos de RSS e desperdício do alocador.
  • Consolide muitos campos minúsculos em hashes que fiquem abaixo dos limiares de listpack. Evite promover para tabelas hash a menos que você realmente precise de objetos grandes.
  • Faça pipeline de MGET/MSET e agrupe escritas. Menos alocações, menos idas e voltas, menos CPU, menor cauda de latência.

Etapa 9: Segmente o cache: camadas quente vs fria

Nem todo cache merece o mesmo silício. Divida seu dataset:

  • Camada quente: instâncias menores, com mais CPU, TTL menor, LFU ativado. Pense em 10–30% das chaves gerando 80–90% dos hits.
  • Camada fria: instâncias com mais RAM, menos CPU, TTL mais longo para itens caros de recalcular, mas pouco acessados.

Faça o roteamento por prefixo. Se você usa serviços gerenciados, isso permite dimensionar corretamente as classes de instância em vez de comprar o “tamanho único que não serve para ninguém”. Se você se auto-hospeda, fixe processos da camada quente em cores e monitore a contenção de linhas de cache.

O que isso significa para custo, latência e raio de impacto

Cada GB eliminado paga três vezes:

  • Custo direto: Menos nós/instâncias mais simples. Muitos times sustentam quedas de 20–40% no custo após o enxugamento e, depois, reinvestem uma fração em clock mais alto ou storage mais rápido para melhorar a latência de cauda.
  • Latência: Conjuntos de trabalho menores cabem melhor nos caches de CPU. Near-caches no app tiram 0,5–2 ms de requisições quentes ao evitar o salto de rede.
  • Raio de impacto: Caches mais enxutos reiniciam mais rápido, replicam menos e fazem failover com estado menor. Seus alertas das 3 da manhã ficam mais curtos.

Você não vai ter a manchete de 100 TB da Cloudflare. Vai conseguir algo mais valioso para uma startup: folga sem heroísmos.

Plano de 90 dias (que realmente sai do papel)

Dias 1–30: Baseline e ganhos rápidos

  • Instrumente: histogramas de tamanho por prefixo, tempo até o último hit, análise de substrings duplicadas nas chaves.
  • Mude para LFU onde fizer sentido e ative a active defrag.
  • Corrija TTLs dos piores casos (por exemplo, reduza de 60m → 10m para chaves cujo último hit de 95% ocorre em menos de 5m).
  • Adicione um near-cache com admissão TinyLFU em um serviço de alto tráfego. Faça A/B por uma semana.

Dias 31–60: Mudanças de esquema e codificação

  • Introduza MessagePack/Protobuf para os dois prefixos que mais consomem memória. Mantenha JSON apenas na borda, se precisar.
  • Normalize chaves com interning de IDs para tenants/regiões. Forneça um caminho reversível de expansão para debug.
  • Comprimir seletivamente (>= 1 KB) com zstd nível 1–3 e dicionários de domínio. Controle com feature flag e monitores de SLO.
  • Escolha armazenamento em hash vs string por objeto com base em microbenchmarks nos seus payloads reais. Consolide a decisão no seu DATA.md ou equivalente.

Dias 61–90: Segmentação e reset de capacidade

  • Separe camadas quente e fria por prefixo. Mova conjuntos caros porém frios para uma camada maior e mais barata; mantenha o núcleo quente pequeno e rápido.
  • Reexecute os baselines. Meta: redução de memória de 30%+, p95 de latência estável ou melhor, taxa de acerto igual ou superior.
  • Dimensione corretamente as instâncias ou remova uma réplica se a segurança da replicação permitir. Prepare planos de rollback e “reaquecimento”.
  • Codifique um Cache Contract: ownership do esquema, orçamentos de memória por prefixo, política de TTL e uma regra de admissão (“não colocamos em cache o que não é reutilizado”). PRs que mudarem o comportamento do cache devem atualizá-lo.

Armadilhas e trade-offs

  • Compressão não é de graça. Se o seu caminho quente já é limitado por CPU, mantenha conjuntos quentes descomprimidos e comprima apenas valores frios ou grandes. Meça p95 e p99.
  • Codificações binárias mudam a ergonomia operacional. Adicione ferramentas para pretty-print em staging. Não volte ao JSON “só para depurar”.
  • A normalização de chaves complica inspeções ad-hoc. Resolva com um sidecar keymap ou uma ferramenta de CLI, não inchando chaves de produção.
  • LFU leva tempo para aprender. No deploy, espere um curto período de adaptação. Use scripts de aquecimento ou prefill se o cold start importar.

Por que isso está em alta agora

Três sinais da indústria convergiram neste trimestre:

  • A vitória de 100 TB da Cloudflare tornou a austeridade de memória “fashion” de novo. E deveria. O GB mais barato é aquele que você nunca aloca.
  • A resistência do Android ao uso de memória por IA lembra todo mundo de que RAM é o gargalo real dos apps modernos—no dispositivo e no seu data plane.
  • A consolidação de fornecedores em hubs de modelos e infraestrutura torna a precificação sob picos e as quotas uma realidade. Você não consegue comprar a saída de um design de cache ruim para sempre.

Os times que tratam caches como produtos—esquemas, contratos, SLOs, orçamentos—vão lançar mais rápido e gastar menos em 2026. Os que não tratarem vão continuar adicionando nós até pagarem um salário real em desperdício de memória.

Onde o nearshore entra

Enxugar o cache é um projeto perfeito para nearshore: alto impacto, mensurável e delimitado. Um pod baseado no Brazil pode trabalhar com seus staff engineers por 6–8 horas de sobreposição, construir os baselines, entregar mudanças de codificação atrás de flags e deixar um playbook de redução de capacidade. Você fica com o código e com os ganhos.

Pontos-chave

  • Overhead domina em Redis/Valkey. Chaves/valores pequenos frequentemente consomem 5–10x seu payload em memória.
  • Normalize chaves e abandone o JSON. Codificações binárias mais interning de IDs geralmente entregam 20–40% de economia.
  • Use TTLs orientadas por dados e LFU. Proteja o conjunto quente e deixe chaves frias morrerem rápido.
  • Adicione um near-cache com TinyLFU nos apps para filtrar duplicidades e reduzir a pressão central em 10–25%.
  • Segmente camadas quente/fria e dimensione instâncias corretamente. Espere 30%+ de redução de memória em 90 dias sem prejudicar a latência.

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation
© 2026 DHDTech.io. All rights reserved.
DHDTech.io · Sheridan, Wyoming
Powered by DHDTech.io DHDTech.io