Introdução
Se você usa Redis em um ambiente compartilhado — seja uma instância usada por múltiplos microserviços, múltiplos times ou múltiplos tenants — uma única chave grande pode degradar o desempenho de todos os outros consumidores ao mesmo tempo. Não é exagero: é como o Redis funciona internamente, e ignorar esse comportamento em produção custa caro.
O problema das chaves grandes (big keys) no Redis existe porque o servidor é single-threaded para operações de dados. Quando um comando acessa ou manipula uma chave que contém centenas de megabytes — seja uma lista com 500 mil elementos, um hash com 200 mil campos ou um string binário de 50 MB — o event loop inteiro fica bloqueado processando essa operação. Todos os outros clientes, de outros serviços completamente diferentes, ficam esperando na fila. Em ambientes compartilhados isso é fatal.
Neste artigo vou mostrar o que caracteriza uma chave grande no Redis, por que o impacto é amplificado em ambientes multi-tenant, como detectar big keys em produção, e as estratégias concretas para eliminar ou mitigar o problema — com exemplos em C# usando StackExchange.Redis.
O que é uma “Big Key” no Redis
O Redis não tem um limite fixo que define quando uma chave é “grande”. A comunidade e a documentação oficial usam o termo big key para descrever chaves cujo custo de processamento ou memória é desproporcional em relação ao workload geral da instância.
Na prática, os limites que costumam surgir como referência são:
| Tipo de Dado | Considerado “grande” a partir de |
|---|---|
| String / Binary | > 5 MB |
| List | > 10.000 elementos ou > 10 MB |
| Hash | > 10.000 campos |
| Set | > 10.000 membros |
| Sorted Set | > 10.000 membros |
| Stream | > 10.000 mensagens pendentes |
Esses números não são absolutos — dependem do hardware, da versão do Redis e do padrão de acesso. Mas são referências sólidas para alertas de monitoramento.
ℹ️ Informação: O Redis 7.0+ introduziu melhorias nas estruturas de dados internas (como Listpack substituindo ZipList), o que mudou ligeiramente os limiares de quando estruturas “promovem” para representações maiores. O conceito de big key, no entanto, permanece o mesmo.
Diferença entre chave grande e chave quente
É comum confundir big key com hot key. São problemas diferentes:
- Big key: chave com muito volume de dados — o problema é o custo de CPU/memória para processar o dado.
- Hot key: chave acessada com altíssima frequência — o problema é a contenção no slot de hash (relevante em Redis Cluster). Um artigo pode ter ambos os problemas ao mesmo tempo, mas as soluções são distintas. Este artigo foca em big keys.
Como o Redis Processa Comandos (e por que isso importa)
Para entender o impacto de big keys, é preciso entender como o Redis executa comandos. O Redis usa um event loop single-threaded para processar operações de dados. Isso significa:
- O servidor recebe um comando de um cliente.
- O event loop executa o comando até o fim antes de processar qualquer outro.
- Somente depois o próximo comando da fila é processado. Essa arquitetura é uma das razões pelo qual o Redis é tão rápido para operações simples: sem locks, sem context-switching entre threads. O custo é que uma operação lenta bloqueia tudo.
Cliente A: GET user:123 → resposta em 0,1ms ✅
Cliente B: LRANGE big-list 0 -1 → 2.3s processando 500k elementos ⚠️
Cliente C: SET config:timeout 30 → aguardando na fila... ⌛
Cliente D: GET session:abc123 → aguardando na fila... ⌛
⚠️ Atenção: Comandos como
LRANGE key 0 -1,SMEMBERS key,HGETALL keyeKEYS *são O(N) — o custo cresce linearmente com o tamanho da estrutura. Em uma chave com 500 mil elementos, isso pode levar segundos.
O modelo de I/O do Redis não é bloqueante — mas o processamento é
É importante separar dois conceitos:
- I/O: o Redis usa multiplexação de I/O (epoll/kqueue), então aceitar conexões e ler dados de rede é não bloqueante.
- Processamento de comandos: a execução do comando em si é single-threaded e bloqueante para os outros clientes. O Redis 6.0 introduziu threads de I/O para leitura e escrita de rede, mas a execução dos comandos ainda é single-threaded no thread principal. O problema das big keys não foi resolvido por essa mudança.
Impacto em Ambientes Compartilhados
Em um ambiente onde uma única instância Redis serve múltiplos microserviços ou múltiplos tenants de um SaaS, o impacto de uma big key é amplificado porque:
1. A latência afeta todos, não só o culpado
Se o serviço de relatórios lê uma chave com 300 mil registros toda hora, isso introduz picos de latência para o serviço de autenticação, o serviço de notificações e qualquer outro que compartilhe a mesma instância — mesmo que esses serviços estejam funcionando perfeitamente.
2. O consumo de memória é disputado
Redis mantém todos os dados em memória. Uma big key que ocupa 500 MB comprime o espaço disponível para todos os outros tenants. Quando o Redis atinge maxmemory, inicia a política de eviction (expulsão de chaves), que pode descartar dados críticos de outros serviços.
3. Operações de snapshot e replicação ficam mais lentas
Quando o Redis gera um RDB snapshot (persistência) ou replica dados para um replica, big keys aumentam o tempo do fork() e o tamanho do arquivo. Em instâncias cloud (AWS ElastiCache, Azure Cache for Redis), isso pode causar falhas de replicação ou penalidades de performance durante a sincronização.
4. Dificuldade de debug e rastreabilidade
Em um ambiente compartilhado, identificar quem criou a big key e qual serviço é o responsável exige instrumentação cuidadosa. Sem namespacing adequado nas chaves, rastrear a origem do problema pode levar horas.
Como Detectar Big Keys em Produção
O Redis oferece ferramentas nativas para detectar big keys. Veja as principais abordagens:
redis-cli –bigkeys
O comando mais simples para uma varredura inicial:
# Varredura completa — use com cuidado em produção (pode ser lenta)
redis-cli --bigkeys
# Com host e porta específicos
redis-cli -h redis.example.com -p 6379 --bigkeys
A saída mostra as maiores chaves por tipo de dado. O problema é que esse comando usa SCAN internamente e pode demorar em instâncias grandes. Em produção, prefira executá-lo em horários de menor carga.
MEMORY USAGE — verificação pontual
Para checar o tamanho de uma chave específica:
# Retorna o tamanho em bytes, incluindo overhead de metadados
MEMORY USAGE minha-chave
# Com nested sampling (para estruturas complexas)
MEMORY USAGE minha-chave SAMPLES 10
SCAN + MEMORY USAGE — varredura automatizada em C
Para monitoramento programático com StackExchange.Redis:
public class RedisBigKeyScanner
{
private const long LimiarBigKeyBytes = 10 * 1024 * 1024; // 10 MB
private readonly IDatabase _db;
private readonly IServer _server;
private readonly ILogger<RedisBigKeyScanner> _logger;
public RedisBigKeyScanner(IConnectionMultiplexer redis, ILogger<RedisBigKeyScanner> logger)
{
_db = redis.GetDatabase();
_server = redis.GetServer(redis.GetEndPoints().First());
_logger = logger;
}
public async Task VarrerBigKeysAsync(string padrao = "*", int tamanhoPagina = 100,
CancellationToken ct = default)
{
// SCAN é não bloqueante — itera em lotes (tamanhoPagina por vez)
await foreach (var key in _server.KeysAsync(pattern: padrao, pageSize: tamanhoPagina)
.WithCancellation(ct))
{
var sizeBytes = await _db.ExecuteAsync("MEMORY", "USAGE", key, "SAMPLES", "5");
if (sizeBytes.IsNull) continue;
var size = (long)sizeBytes;
if (size >= LimiarBigKeyBytes)
_logger.LogWarning("Big key: {Key} | {SizeMB:F2} MB", key, size / (1024.0 * 1024.0));
}
}
}
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/Cache/Redis/
📝 Exemplo: Em um ambiente com 2 milhões de chaves, uma varredura com
pageSize = 500epattern = "relatorio:*"limita o escopo apenas ao namespace problemático, reduzindo o tempo de varredura de horas para minutos.
Redis Latency Monitoring
Para detectar o impacto em tempo real, ative o monitoramento de latência:
# Ativar no redis.conf ou via CONFIG SET
CONFIG SET latency-monitor-threshold 100
# Ver eventos de latência
LATENCY LATEST
LATENCY HISTORY command
Estratégias para Evitar Big Keys
Detectar o problema é o primeiro passo. Resolver é mais interessante. Existem quatro abordagens principais:
1. Fragmentação de dados (Sharding)
Em vez de uma única chave grande, distribuir os dados em múltiplas chaves menores usando um campo de particionamento:
public class RedisHashSharded
{
private readonly IDatabase _db;
private const int QuantidadeShards = 16; // potência de 2 para distribuição uniforme
public RedisHashSharded(IConnectionMultiplexer redis) => _db = redis.GetDatabase();
private string ObterChaveShard(string chaveBase, string campo)
{
var indiceShard = Math.Abs(campo.GetHashCode()) % QuantidadeShards;
return $"{chaveBase}:shard:{indiceShard}";
}
public async Task GravarAsync(string chaveBase, string campo, string valor)
{
var chaveShard = ObterChaveShard(chaveBase, campo);
await _db.HashSetAsync(chaveShard, campo, valor);
}
public async Task<string?> LerAsync(string chaveBase, string campo)
{
var chaveShard = ObterChaveShard(chaveBase, campo);
var resultado = await _db.HashGetAsync(chaveShard, campo);
return resultado.HasValue ? resultado.ToString() : null;
}
// LerTodosAsync paralleliza a leitura dos QuantidadeShards shards — código completo no repositório
}
2. Serialização eficiente com compressão
Um dos maiores culpados por strings grandes é a serialização ingênua de objetos complexos. Trocar JSON por um formato binário + compressão reduz dramaticamente o tamanho:
public static class RedisSerializerExtensions
{
public static async Task GravarComprimidoAsync<T>(
this IDatabase db, string chave, T valor, TimeSpan? expiracao = null)
{
using var stream = new MemoryStream();
await using (var gzip = new GZipStream(stream, CompressionLevel.Optimal))
await JsonSerializer.SerializeAsync(gzip, valor);
await db.StringSetAsync(chave, stream.ToArray(), expiracao);
}
public static async Task<T?> LerComprimidoAsync<T>(this IDatabase db, string chave)
{
var valor = await db.StringGetAsync(chave);
if (!valor.HasValue) return default;
using var stream = new MemoryStream((byte[])valor!);
await using var gzip = new GZipStream(stream, CompressionMode.Decompress);
return await JsonSerializer.DeserializeAsync<T>(gzip);
}
}
💡 Dica: Compressão GZip típica reduz payloads JSON em 60–80%. Um objeto de 5 MB pode cair para 800 KB — o que muda a classificação de “big key” para um tamanho gerenciável. Mas atenção: compressão tem custo de CPU, avalie o trade-off para dados acessados com alta frequência.
3. TTL obrigatório para chaves de cache
Chaves sem TTL em ambientes compartilhados tendem a acumular dados indefinidamente. Implemente uma política de TTL obrigatória:
public class RedisCacheComPoliticaTtl
{
private readonly IDatabase _db;
private static readonly TimeSpan TtlPadrao = TimeSpan.FromHours(1);
private static readonly TimeSpan TtlMaximo = TimeSpan.FromHours(24);
public RedisCacheComPoliticaTtl(IConnectionMultiplexer redis)
=> _db = redis.GetDatabase();
public async Task GravarAsync(string chave, string valor, TimeSpan? ttl = null)
{
// Nunca permite TTL nulo — usa o padrão se não especificado
var ttlEfetivo = ttl.HasValue
? TimeSpan.FromTicks(Math.Min(ttl.Value.Ticks, TtlMaximo.Ticks))
: TtlPadrao;
await _db.StringSetAsync(chave, valor, ttlEfetivo);
}
}
4. Namespacing e segregação por tenant
Em ambientes multi-tenant, use prefixos obrigatórios para identificar a origem das chaves. Isso facilita o diagnóstico e permite políticas de limpeza seletivas:
// Padrão de chave: {ambiente}:{tenant}:{dominio}:{entidade}:{id}
// Exemplo: prod:acme-corp:relatorio:mensal:2026-05
public static class RedisKeyBuilder
{
public static string Construir(
string tenant,
string dominio,
string entidade,
string id,
string ambiente = "prod")
{
return $"{ambiente}:{Sanitizar(tenant)}:{Sanitizar(dominio)}:{Sanitizar(entidade)}:{id}";
}
private static string Sanitizar(string segmento) =>
segmento.ToLowerInvariant().Replace(" ", "-").Replace(":", "");
}
Exemplo Prático
Vou mostrar um cenário real: um serviço de relatórios que carregava todos os registros de transações do mês em uma única chave Redis para “cache” — e estava derrubando o desempenho dos outros serviços no mesmo cluster.
O problema original
// ❌ CÓDIGO PROBLEMÁTICO — NÃO FAÇA ISSO
public async Task<List<Transacao>> ObterTransacoesMensaisAsync(
int clienteId,
int mes,
int ano)
{
var cacheKey = $"transacoes:{clienteId}:{mes}/{ano}";
var cached = await _cache.StringGetAsync(cacheKey);
if (cached.HasValue)
{
// Desserializa 300.000 transações em memória de uma vez
return JsonSerializer.Deserialize<List<Transacao>>(cached!)!;
}
// Busca 300.000 registros do banco
var transacoes = await _db.Transacoes
.Where(t => t.ClienteId == clienteId
&& t.Data.Month == mes
&& t.Data.Year == ano)
.ToListAsync();
// Serializa 300.000 objetos para um JSON de ~80 MB e salva no Redis
// Sem TTL → fica para sempre
await _cache.StringSetAsync(
cacheKey,
JsonSerializer.Serialize(transacoes));
return transacoes;
}
Esse código cria uma chave que pode ter 80 MB no Redis, sem TTL, e é lida inteira a cada acesso. Cada leitura bloqueia o event loop por centenas de milissegundos.
A solução refatorada
// ✅ SOLUÇÃO CORRETA — Cache paginado com TTL obrigatório
public class TransacaoCache
{
private readonly IDatabase _redis;
private const int TamanhoPagina = 100;
private const string PrefixoChave = "tx";
public TransacaoCache(IConnectionMultiplexer redis) => _redis = redis.GetDatabase();
public async Task GravarPaginaAsync(int clienteId, int mes, int ano, int pagina,
IEnumerable<Transacao> transacoes)
{
var chave = ConstruirChavePagina(clienteId, mes, ano, pagina);
var json = JsonSerializer.Serialize(transacoes.Take(TamanhoPagina));
// TTL de 30 minutos — dado de relatório, não precisa ser eterno
await _redis.StringSetAsync(chave, json, TimeSpan.FromMinutes(30));
}
public async Task<List<Transacao>?> ObterPaginaAsync(int clienteId, int mes, int ano, int pagina)
{
var chave = ConstruirChavePagina(clienteId, mes, ano, pagina);
var cache = await _redis.StringGetAsync(chave);
return cache.HasValue ? JsonSerializer.Deserialize<List<Transacao>>((string)cache!) : null;
}
// Cada chave contém apenas TamanhoPagina transações (~30 KB)
private static string ConstruirChavePagina(int clienteId, int mes, int ano, int pagina) =>
$"{PrefixoChave}:{clienteId}:{mes}-{ano}:p{pagina}";
}
Com essa abordagem, cada chave passa de 80 MB para ~30 KB. O event loop é liberado em microssegundos, e os outros serviços compartilhados deixam de sofrer latência por conta do serviço de relatórios.
Dicas e Boas Práticas
Defina limiares de alerta no CI/CD: antes de um deploy, verifique se novos padrões de chave respeitam os limites de tamanho. Uma pipeline de testes de carga com
redis-cli --bigkeyspode prevenir o problema antes de chegar à produção.Use TTL em absolutamente todas as chaves: em ambientes compartilhados, uma chave sem TTL é uma chave que vai crescer indefinidamente. Configure
maxmemory-policy allkeys-lruno Redis para que, mesmo que alguma chave escape sem TTL, o Redis possa descartá-la quando necessário.Nunca use
KEYS *em produção: o comandoKEYSbloqueia o Redis enquanto varre todo o keyspace. Use sempreSCANcomMATCHeCOUNTpara varreduras iterativas e não bloqueantes. OStackExchange.Redisexpõe esse comportamento através deIServer.KeysAsync().Monitore
redis-cli --latency-history: latências acima de 50ms recorrentes são sinal de big keys ou hot keys. Configure alertas de latência no Redis e correlacione com os padrões de chave que estão sendo acessados no momento.Separe workloads em instâncias distintas quando possível: se um serviço de relatórios inevitavelmente precisa de chaves grandes, isole-o em uma instância Redis dedicada. O custo de uma instância extra é muito menor que o impacto de latência nos serviços críticos de um ambiente compartilhado.
Prefira estruturas nativas do Redis ao invés de strings JSON grandes: um
HASHcom 1.000 campos é mais eficiente que umSTRINGcom um JSON de 1.000 propriedades. O Redis pode acessar campos individuais do hash sem deserializar a estrutura toda, economizando CPU e memória.Revise o design ao usar Redis como banco de dados: Redis é excelente como cache de curta duração, pub/sub e contadores. Quando começa a receber coleções completas de entidades para “persistência temporária”, é um sinal de que o design precisa ser revisado — provavelmente um banco relacional ou documento é a ferramenta certa para aquele dado.
Resumo Objetivo
- Big key no Redis — chave cujo volume de dados é desproporcional ao workload; limites práticos são 5 MB para strings, 10.000 elementos para listas/hashes/sets. A partir desses valores, operações O(N) bloqueiam o event loop por centenas de milissegundos.
- Event loop single-threaded — o Redis processa um comando por vez no thread principal (mesmo no Redis 6.0+ com I/O threads). Uma big key bloqueia todos os outros clientes da instância enquanto é processada.
- Ambientes compartilhados — em instâncias Redis usadas por múltiplos microserviços ou tenants, uma big key criada por um único serviço introduz latência para todos os outros, independentemente do seu comportamento.
-
Comandos O(N) perigosos —
LRANGE key 0 -1,SMEMBERS key,HGETALL key,KEYS *eSUNIONSTOREsão os principais vetores de bloqueio quando aplicados a chaves grandes. Prefira variantes paginadas comoHSCAN,SSCANeZSCAN. -
Detecção em produção —
redis-cli --bigkeysrealiza varredura por tipo;MEMORY USAGE key SAMPLES Nretorna o tamanho exato em bytes incluindo overhead;LATENCY LATESTidentifica comandos com latência anômala. - Fragmentação como solução — dividir uma big key em múltiplas chaves menores usando sharding por campo elimina o bloqueio e permite paralelismo na leitura. Um hash com 100.000 campos pode ser dividido em 16 shards de ~6.250 campos cada.
-
TTL obrigatório — toda chave em ambiente compartilhado deve ter expiração. A ausência de TTL é a principal causa de acúmulo de big keys em produção. Configure
maxmemory-policy allkeys-lrucomo rede de segurança.
Leia Também
- Gargalo em Banco de Dados: Mensageria e Paginação — estratégias para resolver gargalos de escrita e leitura com mensageria e paginação eficiente no EF Core 8+.
- .NET Worker e Background Service: Processamento de Alto Volume — como processar grandes volumes de dados em background sem sobrecarregar os serviços principais.
- Programação Assíncrona em C#: async/await e Threads — fundamentos de programação assíncrona em C# para evitar bloqueios em operações de I/O, incluindo acesso a caches.
- Paginação em APIs REST com C# e EF Core: Guia Prático — padrões de paginação para evitar retornar grandes volumes de dados de uma vez.
Referências
- Redis Big Keys — Documentação Oficial — especificações de tipos de dados e comportamento interno do Redis por versão.
- Redis Memory Optimization — Best Practices — guia oficial de otimização de memória no Redis, cobrindo encoding interno de estruturas de dados.
- StackExchange.Redis — Documentação — documentação da biblioteca cliente .NET para Redis, incluindo uso de SCAN e MEMORY USAGE.
- Redis Latency Problems — Troubleshooting Guide — guia oficial de diagnóstico de latência no Redis, com foco em comandos bloqueantes.
- Anti-Patterns in Redis Usage — Redis Labs Blog — anti-padrões comuns de uso do Redis identificados pela equipe da Redis Ltd., incluindo big keys e hot keys.
- Redis SCAN command — documentação do comando SCAN com exemplos de varredura não bloqueante para uso em produção. 📬
👉 Artigo completo com todos os exemplos de código: Redis: Big Keys Destroem o Desempenho Compartilhado




















