Todo sistema começa pequeno. Mil registros, dez mil, cem mil.
Tudo responde rápido. O usuário clica e a informação aparece. Você nem pensa em performance porque não precisa.
Aí o sistema cresce.
O banco acumula milhões de registros e um belo dia, alguém abre o dashboard, aplica um filtro de janeiro a dezembro e espera.
E espera. E toma timeout. E dá F5. E abandona.
A reação natural é culpar o banco de dados, trocar o plano, adicionar índice, jogar mais RAM... Mas o problema real é outro: você está fazendo o processamento pesado no momento em que o usuário pede a informação. E isso não escala.
Nesse artigo eu vou te mostrar o padrão que sistemas grandes usam pra lidar com milhões (ou bilhões) de registros sem que o usuário perceba. E o melhor: não precisa ser um engenheiro do Google pra aplicar isso.
O Problema: Processar na Hora Errada
Pra entender o problema, imagina um cenário simples.
Você tem uma tabela de transações financeiras. Cada depósito, cada saque, cada transferência gera um registro. Pra saber o saldo, o sistema precisa somar todas as transações desde o início da conta.
Com mil transações, isso é instantâneo. Com cem milhões? Inviável em tempo real.
Visualmente, o fluxo problemático é esse:
O problema não é o banco. É a estratégia. Você está pedindo pro banco fazer um trabalho gigantesco toda vez que alguém clica num botão.
A Solução: Processar Antes, Servir Rápido
A ideia é simples: separe o processamento pesado da resposta ao usuário.
Um processo roda em background — pode ser uma vez por hora, uma vez por dia, depende do caso — e consolida os dados numa tabela de rollup. Quando o usuário pede o saldo, o sistema consulta o rollup e calcula apenas as transações que aconteceram depois do último processamento.
Ao invés de varrer anos de dados, ele varre horas. Ou minutos.
Percebe a diferença? O trabalho pesado já foi feito. A resposta pro usuário é quase instantânea.
Como Funciona uma Tabela de Rollup
A tabela de rollup é basicamente um checkpoint. Ela guarda o resultado de um cálculo pesado num ponto específico no tempo.
Estrutura da tabela
1CREATE TABLE saldo_rollups ( 2 id BIGINT PRIMARY KEY IDENTITY, 3 conta_id BIGINT NOT NULL, 4 saldo_acumulado DECIMAL(18,2) NOT NULL, 5 calculado_em DATETIME2 NOT NULL, 6 transacao_ate_id BIGINT NOT NULL, -- última transação incluída no cálculo 7 8 INDEX IX_rollup_conta (conta_id, calculado_em DESC) 9);
O job que gera o rollup
1public class RollupJob 2{ 3 private readonly AppDbContext _db; 4 5 public RollupJob(AppDbContext db) => _db = db; 6 7 public async Task ExecutarAsync() 8 { 9 // Pega todas as contas que têm transações novas 10 var contasComMovimentacao = await _db.Transacoes 11 .Where(t => t.Data > _db.SaldoRollups 12 .Where(r => r.ContaId == t.ContaId) 13 .Max(r => (DateTime?)r.CalculadoEm) ?? DateTime.MinValue) 14 .Select(t => t.ContaId) 15 .Distinct() 16 .ToListAsync(); 17 18 foreach (var contaId in contasComMovimentacao) 19 { 20 // Busca o último rollup dessa conta 21 var ultimoRollup = await _db.SaldoRollups 22 .Where(r => r.ContaId == contaId) 23 .OrderByDescending(r => r.CalculadoEm) 24 .FirstOrDefaultAsync(); 25 26 var saldoAnterior = ultimoRollup?.SaldoAcumulado ?? 0; 27 var dataCorte = ultimoRollup?.CalculadoEm ?? DateTime.MinValue; 28 29 // Soma só as transações novas 30 var delta = await _db.Transacoes 31 .Where(t => t.ContaId == contaId && t.Data > dataCorte) 32 .SumAsync(t => t.Valor); 33 34 // Grava o novo rollup 35 _db.SaldoRollups.Add(new SaldoRollup 36 { 37 ContaId = contaId, 38 SaldoAcumulado = saldoAnterior + delta, 39 CalculadoEm = DateTime.UtcNow 40 }); 41 } 42 43 await _db.SaveChangesAsync(); 44 } 45}
A consulta rápida
1public async Task<decimal> ObterSaldoAsync(long contaId) 2{ 3 // Pega o último rollup 4 var rollup = await _db.SaldoRollups 5 .Where(r => r.ContaId == contaId) 6 .OrderByDescending(r => r.CalculadoEm) 7 .FirstOrDefaultAsync(); 8 9 var saldoBase = rollup?.SaldoAcumulado ?? 0; 10 var dataCorte = rollup?.CalculadoEm ?? DateTime.MinValue; 11 12 // Soma só o que veio depois 13 var delta = await _db.Transacoes 14 .Where(t => t.ContaId == contaId && t.Data > dataCorte) 15 .SumAsync(t => t.Valor); 16 17 return saldoBase + delta; 18}
O ponto chave: a query do rollup varre milhões de registros, mas roda em background, sem pressa. A query do usuário varre dezenas ou centenas de registros — só o delta desde o último processamento.
Background Jobs: O Trabalho Pesado Acontece nos Bastidores
O rollup é um tipo de background job. Mas o padrão vai muito além de saldos financeiros.
Cenário: Monitoramento Industrial com Milhares de Sensores
Imagina uma operação industrial com 5 mil sensores: temperatura, pressão, vibração, umidade. Cada sensor manda dados o tempo todo. Se você tentar gravar tudo direto num banco relacional e calcular em tempo real, vai estourar.
O que sistemas assim fazem:
Cada camada resolve um problema diferente:
| Camada | O que faz | Por que existe |
|---|---|---|
| Message Broker | Absorve o volume de entrada dos sensores | Desacopla produtores de consumidores. Se um worker cai, as mensagens ficam na fila |
| Workers | Processam dados em paralelo, cada um especializado | Distribui a carga. Se temperatura demora mais, não trava pressão |
| Rollup DB | Consolida dados históricos em intervalos | Transforma bilhões de leituras em milhares de agregações consultáveis |
| Cache | Guarda os resultados mais acessados | Evita consultas repetidas ao banco. Dashboard abre em milissegundos |
Outros cenários comuns
O padrão se repete em vários contextos:
Anatomia de um Sistema com Background Jobs
Quando você junta essas peças, a arquitetura completa fica assim:
Dois mundos separados:
- Camada do usuário: rápida, leve, só consulta dados pré-processados
- Camada de background: pesada, sem pressa, faz o trabalho bruto quando ninguém está esperando
Quando Vale a Pena (e Quando Não Vale)
Essa arquitetura não é pra todo mundo. Se seu sistema tem poucos dados e responde bem, adicionar workers e rollups é over-engineering puro.
A regra é simples: se o processamento é pesado e o resultado não muda a cada segundo, não faz sentido recalcular tudo toda vez que alguém pede. Processa antes, guarda o resultado, serve rápido.
Sinais de que você precisa dessa arquitetura
| Sinal | O que acontece | O que fazer |
|---|---|---|
| Queries lentas que pioram com o tempo | Dashboard que levava 200ms agora leva 8s | Rollup + cache |
| Timeout em relatórios | Usuário clica em "gerar relatório" e toma 504 | Background job + notificação quando pronto |
| CPU do banco no teto em horário comercial | Consultas analíticas competindo com operações transacionais | Separar leitura pesada em jobs off-peak |
| Usuários reclamando de lentidão | O sistema "trava" em horários de pico | Cache + pré-processamento |
Na Prática: Ferramentas por Stack
As ferramentas variam conforme a stack, mas o conceito é sempre o mesmo.
.NET
1// Com Hangfire — agendar um job recorrente 2RecurringJob.AddOrUpdate<RollupJob>( 3 "rollup-saldos", 4 job => job.ExecutarAsync(), 5 Cron.Hourly); // roda a cada hora
Node.js
1// Com BullMQ — adicionar job numa fila 2import { Queue } from 'bullmq'; 3 4const rollupQueue = new Queue('rollup'); 5 6// Agendar job recorrente 7await rollupQueue.add('consolidar-saldos', 8 { tipo: 'saldos' }, 9 { repeat: { every: 3600000 } } // a cada 1 hora 10);
Python
1# Com Celery — task periódica 2from celery import Celery 3from celery.schedules import crontab 4 5app = Celery('tasks') 6 7@app.on_after_configure.connect 8def setup_periodic_tasks(sender, **kwargs): 9 sender.add_periodic_task( 10 crontab(minute=0), # a cada hora cheia 11 gerar_rollup.s() 12 ) 13 14@app.task 15def gerar_rollup(): 16 # lógica de consolidação aqui 17 pass
Outras opções
| Ferramenta | Stack / Contexto | Tipo |
|---|---|---|
| Hangfire | .NET | Library (roda dentro da app) |
| Quartz.NET | .NET | Library (mais configurável) |
| BullMQ | Node.js | Fila + workers com Redis |
| Celery | Python | Task queue distribuída |
| Sidekiq | Ruby | Background jobs com Redis |
| Azure Functions | Cloud (Azure) | Serverless com timer trigger |
| AWS Lambda + EventBridge | Cloud (AWS) | Serverless com schedule |
| Cron job | Linux / qualquer | O clássico. Simples e funciona |
O conceito é o mesmo independente da tecnologia: separar o processamento pesado da resposta ao usuário. Um acontece em background, no tempo dele. O outro acontece na hora, e precisa ser rápido.
Quem Faz Isso no Mundo Real
Stack Overflow, Reddit, Instagram, Amazon, Mercado Livre. Todos processam bilhões de dados por dia. Nenhum deles faz o cálculo na hora que o usuário clica.
- Netflix: pré-calcula recomendações em batch jobs massivos. Quando você abre o app, o catálogo personalizado já está pronto.
- Mercado Livre: consolida métricas de vendedores (reputação, tempo de envio, taxa de reclamação) em background. Se calculasse em tempo real pra cada busca, a página nunca carregaria.
- Nubank: saldo, extrato, limites — tudo consolidado em pipelines de dados que rodam continuamente. O app só consulta o resultado.
Se eles fizessem o processamento na hora do clique, você já teria reclamado.
Resumo Visual
Conclusão
Parece complexo visto de fora. Mas cada peça encaixa na outra.
O broker absorve o volume. Os workers distribuem o processamento. O rollup consolida. O cache serve rápido. E o usuário nem percebe que por trás de um clique tem uma arquitetura inteira trabalhando nos bastidores.
Você não precisa implementar tudo de uma vez. Começa pelo rollup. Coloca um job simples que roda de hora em hora. Mede a diferença. Quando sentir necessidade, adiciona cache. Depois workers. Cada camada resolve um problema novo.
O segredo não é usar a ferramenta mais sofisticada. É entender quando processar. E a resposta quase sempre é: não na hora que o usuário pediu.