Batch processing, Node.js, Kafka, MongoDB
Como destravei um pipeline Node.js/Kafka sem reescrever a arquitetura
Em produção, batches grandes travavam o event loop, faziam a memória subir e derrubavam o ritmo do processamento. O ajuste veio na iteração, sem reescrever a arquitetura.
Publicado em 2026-04-05 · Atualizado em 2026-04-05
Resumo do caso
- Problema
- O loop de processamento empilhava trabalho assíncrono demais e o pipeline perdia fôlego em batches grandes.
- Contexto
- Sistema interno com escrita em MongoDB, publicação em Kafka e etapas de enriquecimento por API.
- Decisão
- Trocar o padrão de iteração por async generators com `for await...of`, sem mexer na arquitetura como um todo.
- Resultado
- Os health checks voltaram a responder, a memória parou de crescer com o lote e o throughput deixou de degradar.
Quando o problema apareceu, a leitura mais óbvia parecia fazer sentido: sistema rodando o tempo todo, lote grande, dependências externas, pressão em produção. A conversa começou no lugar de sempre: subir mais pods, dar mais memória e discutir uma reescrita.
O ponto é que o gargalo não estava em Kafka, nem no MongoDB, nem na quantidade de recurso alocado. Estava no jeito como o processamento percorria os itens. O código empilhava operações assíncronas rápido demais e deixava o runtime tentando acompanhar tudo ao mesmo tempo.
Os sintomas
O processamento de batches ainda usava um padrão de iteração que, com pouco volume, parecia inofensivo. O problema só aparecia quando o lote crescia. Nessa hora, o comportamento era sempre parecido: os primeiros itens passavam bem, depois o ritmo caía, os health checks começavam a falhar e a memória subia junto.
O serviço não “caía de uma vez”. Ele ia ficando pesado. O event loop perdia fôlego, o producer do Kafka acumulava pressão, o driver do MongoDB passava a responder pior e qualquer latência extra nas APIs de enriquecimento virava atraso em cadeia. Quando você olha só para fora, parece falta de infra. Quando olha o fluxo por dentro, fica claro que tinha trabalho demais sendo disparado sem controle.
Onde realmente estava o problema
A suspeita inicial do time era razoável: talvez o sistema tivesse chegado no limite. Só que o desenho da iteração não combinava com a forma como o Node.js trabalha. O runtime não estava “sem poder”. Ele estava sendo pressionado do jeito errado.
Dar mais pod ou mais memória provavelmente compraria algum tempo, mas não resolveria a causa. A pergunta útil deixou de ser “quanto recurso falta?” e passou a ser “onde esse fluxo está perdendo controle?”
Foi aí que o diagnóstico andou. Em vez de continuar olhando dashboard de infraestrutura, eu fui direto ao loop principal do processamento. O padrão era simples: o código seguia criando novas operações assíncronas antes de as anteriores terminarem. Em lote pequeno isso passava. Em lote grande, virava acúmulo, disputa por I/O e degradação progressiva.
A reescrita que não aconteceu
Havia proposta para reorganizar o fluxo inteiro, separar etapas de outro jeito e colocar mais componentes no caminho. Não era uma ideia absurda. O problema é que o custo era alto demais para um defeito que estava concentrado em um ponto do processamento.
O sistema não precisava mudar de forma. Precisava parar de despejar trabalho assíncrono sem backpressure. Quando isso ficou claro, a decisão ficou mais simples também: em vez de semanas de reestruturação, dava para atacar o ponto exato que estava sufocando o runtime.
O que mudou no código
A mudança foi trocar a iteração por async generators com for await...of. Na prática, isso devolveu ordem ao fluxo. Cada item passou a entrar no processamento num ritmo que o serviço conseguia sustentar, em vez de o código sair disparando trabalho novo sem critério.
Streams eram uma alternativa possível, mas pediam uma reorganização maior do sistema. Para esse caso, async generators resolveram a parte importante com uma mudança localizada e fácil de entender para o time. O código não ficou mais “sofisticado”. Ficou mais correto para o tipo de carga que ele processava.
No fim das contas
O ganho aqui não veio de uma tecnologia nova nem de uma mudança grande de arquitetura. Veio de olhar o problema no nível certo. Antes de assumir gargalo de escala, fez mais sentido revisar como o runtime estava sendo alimentado.
Esse foi um caso em que algumas horas lendo o fluxo com calma valeram mais do que uma reescrita apressada. O problema não era “Node.js não aguenta”. O problema era o jeito como o código estava usando concorrência. Corrigido isso, o pipeline voltou a responder como deveria.
Antes e depois
| Aspecto | Antes | Depois |
|---|---|---|
| Event loop | Bloqueado durante batches grandes | Estável, com health checks respondendo |
| Memória | Picos proporcionais ao tamanho do batch | Consumo constante, independente do volume |
| Throughput | Degradação progressiva ao longo do lote | Velocidade consistente do primeiro ao último item |
| Infraestrutura | Pressão para escalar recursos | Zero mudança de infra para resolver o problema |
Stack e ambiente
- Node.js
- Kafka
- MongoDB
- JavaScript/TypeScript
- Pipeline interno de alto volume
Em uma frase
Parecia falta de infraestrutura, mas era só um fluxo assíncrono mal controlado. Corrigido o loop, o resto voltou para o lugar.
Antes de escalar recurso, vale olhar se o código está entregando trabalho no ritmo que o runtime consegue absorver.