Streams: gigabytes de dados com megabytes de memória
readFile de 2GB derruba o servidor; um pipeline de streams processa o mesmo arquivo com 64KB por vez. Tipos, backpressure, Transform próprio e os tropeços clássicos.
O jeito ingênuo de processar um arquivo de 2GB no Node.js cabe numa linha, e derruba o servidor com ela:
const dados = await fs.promises.readFile("vendas-2025.csv"); // 💥 2GB na RAM
Multiplique por três usuários simultâneos e o processo morre por falta de memória. O jeito certo também cabe em poucas linhas, e processa o mesmo arquivo usando alguns megabytes: são os streams, o recurso mais subestimado do Node.
A ideia: processar o rio, não o lago
Stream é dado em movimento: em vez de carregar tudo para depois processar, você processa pedaço a pedaço (chunks, tipicamente de 64KB) conforme eles passam. A memória usada não depende do tamanho do arquivo; depende do tamanho do pedaço.
Os quatro tipos, direto da documentação oficial (nodejs.org):
- Readable: fonte de dados (arquivo, requisição HTTP, resposta do S3).
- Writable: destino (arquivo, resposta HTTP, socket).
- Transform: o meio de campo: recebe, transforma, repassa (gzip, parse, filtro).
- Duplex: lê e escreve (sockets).
E o operador que junta tudo com segurança é o pipeline:
const { pipeline } = require("node:stream/promises");
const fs = require("node:fs");
const zlib = require("node:zlib");
// Comprime 2GB usando ~64KB de memória por vez:
await pipeline(
fs.createReadStream("vendas-2025.csv"),
zlib.createGzip(),
fs.createWriteStream("vendas-2025.csv.gz")
);
Use pipeline, não o velho .pipe(): ele propaga erros corretamente e destrói todos os streams se qualquer um falhar, o vazamento clássico do .pipe() encadeado.
Backpressure: a mágica que ninguém vê
O detalhe que separa streams de um simples loop: e se a fonte produz mais rápido do que o destino consome? Ler do disco é rápido; enviar pela rede é lento. Sem controle, os pedaços se acumulam na memória e voltamos ao problema original.
Streams resolvem isso com backpressure (contrapressão): quando o buffer interno do destino enche (o limite é o highWaterMark), o stream de escrita avisa, e a leitura pausa sozinha até o destino drenar. O fluxo inteiro anda no ritmo do elo mais lento, automaticamente. Com pipeline, você ganha isso de graça; implementando na mão com eventos, é exatamente onde todo mundo erra.

Um Transform seu, na prática
O poder real aparece quando você encaixa a sua lógica no meio do fluxo:
const { Transform } = require("node:stream");
// Filtra linhas de um CSV gigante: só vendas acima de R$ 1.000
const filtraGrandes = new Transform({
transform(chunk, _enc, done) {
const linhas = chunk.toString().split("\n");
const grandes = linhas.filter((l) => Number(l.split(";")[2]) > 1000);
done(null, grandes.join("\n"));
},
});
await pipeline(
fs.createReadStream("vendas-2025.csv"),
filtraGrandes,
fs.createWriteStream("vendas-grandes.csv")
);
(Em produção, some um parser de CSV em stream para tratar linhas quebradas entre chunks; o esqueleto é este.)
Onde streams pagam a conta todo dia
- Upload/download direto para o storage: o arquivo do usuário atravessa sua API em pedaços rumo ao S3, sem nunca existir inteiro na memória do servidor.
- Exportações e relatórios: o CSV de 500 mil linhas começa a chegar no navegador em milissegundos, gerado sob demanda, em vez de estourar a RAM montando tudo antes.
- ETL e integrações: ler do banco em cursor, transformar, gravar no destino: pipelines de dados dentro do Node com memória constante.
- Proxies e gateways: repassar corpos de requisição sem bufferizar (aliás,
reqeresdo HTTP já são streams; muita gente bufferiza sem necessidade o que já chegava fluindo).
É também o elo com o resto da série: streams mantêm o event loop livre (cada chunk é um callback curtinho), evitam o JSON.parse gigante que trava tudo, e reduzem a memória por processo, o que multiplica bem com o cluster.
Os tropeços clássicos
- Ignorar erros: stream sem tratamento de erro derruba o processo.
pipelineresolve;.pipe()solto, não. - Bufferizar no meio: acumular chunks num array "para processar depois" cancela o benefício inteiro. Se precisa de tudo em memória, streams não eram a ferramenta (ou a lógica precisa mudar).
- Chunk não é unidade de dado: um chunk pode terminar no meio de uma linha, de um caractere UTF-8, de um objeto JSON. Use parsers em stream para o formato, não
splitingênuo. - Esquecer que o consumidor pode sumir: cliente HTTP desconectou no meio do download? Sem
pipeline, o stream de origem continua lendo para ninguém.
Memória constante como requisito
Se a sua API tem endpoints de upload, exportação ou integração que "de vez em quando estouram a memória", o diagnóstico costuma ser o mesmo: dado grande sendo bufferizado onde deveria fluir. Redesenhar esses caminhos com streams é trabalho que fazemos no serviço de Desenvolvimento Web, Mobile e Desktop.
Qual endpoint seu carrega arquivo inteiro na memória hoje? Descreva o fluxo para a IA da Rabelo Digital, aqui no canto da tela, e ela desenha o pipeline equivalente. Sem formulário, sem espera.


Deixe um Comentário