Streams: gigabytes de dados com megabytes de memória

Streams: gigabytes de dados com megabytes de memória

Robson Rabelo - 12 de julho de 2026 - 1 visualizações

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.

Streams e backpressure no Node.js

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, req e res do 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

  1. Ignorar erros: stream sem tratamento de erro derruba o processo. pipeline resolve; .pipe() solto, não.
  2. 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).
  3. 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 split ingênuo.
  4. 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.


Leia também

Compartilhar:

Deixe um Comentário

Categorias

Posts Recentes