@jeff_drumgod
EN

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

Event loop estável durante batches grandes Consumo de memória constante Refactor em horas, não semanas

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.

Links úteis neste contexto