Processando CSVs grandes com Node.js Streams sem estourar memória
Por John Pires · 10 de março de 2026

Processando CSVs grandes com Node.js Streams sem estourar memória
Contexto: quando você vai usar isso
Por que CSVs grandes são um problema
Arquivos CSV são onipresentes: exportações de sistemas legados, relatórios, dumps de dados e integrações. O problema aparece quando o arquivo não cabe na memória do servidor. Ler tudo num buffer com fs.readFile significa estourar RAM, matar o processo ou degradar todo o serviço. Para quem está começando, pense em carregar um caminhão de pedras parcialmente desmontado em cima de uma mesa frágil: a mesa quebra. Streams permitem carregar pedra por pedra.
Quando usar streams em vez de ler o arquivo inteiro
Use streams quando o arquivo for grande (megabytes para gigabytes), quando a latência de processamento por linha importar, quando quer inserir linhas em lote numa DB, ou expor uma API que aceita upload e processa sem guardar o arquivo. Se a sua aplicação precisa de random access ao arquivo inteiro, streams não são suficiente por si — mas para transformação linear e ingestão, são ideais.
Streams também trazem backpressure: o consumidor informa ao produtor quanta velocidade consegue processar. Isso evita encher buffers e/ou filas internas.
Explicação visual com diagramas mentais
Fluxo mental simples
Imagine uma esteira transportadora. O arquivo é uma pilha de caixas. A esteira move caixa por caixa, um processador inspeciona cada caixa e coloca o resultado em outra esteira de saída. Se o processo de inspeção for lento, a esteira pode parar para não acumular caixas em excesso. Essa é a essência do backpressure.
Componentes na arquitetura
Os componentes básicos que veremos:
- Readable stream: leitura do arquivo ou do upload.
- Transform stream: parse e transformação linha a linha.
- Writable stream: persistência (arquivo, DB, resposta HTTP).
- Worker threads (opcional): descarregar CPU-bound.
Tip: stream não é mágica; ainda precisa tratar erros, fechar recursos e respeitar backpressure para evitar vazamentos de memória.
Primeiro exemplo simples
Objetivo do exemplo
Exemplo minimal: ler um CSV grande linha por linha, transformar cada linha em objeto e contar registros. Não usa bibliotecas externas, apenas readline para demonstrar um caminho inicial e seguro em memória.
Código: contagem de linhas com readline
O código abaixo abre um arquivo CSV e processa linha a linha, sem carregar tudo em memória. Funciona bem para arquivos grandes e é um ponto de partida para iniciantes.
// O que este arquivo faz: abre um CSV e conta as linhas processadas, parseando columnas simples
const fs = require('fs');
const readline = require('readline');
async function countCsvLines(path) {
const stream = fs.createReadStream(path);
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
let count = 0;
for await (const line of rl) {
// processamento simples: ignorar cabeçalho
if (count === 0) {
count++;
continue;
}
// aqui você pode fazer parse simples: const cols = line.split(',');
count++;
}
return count - 1; // subtrai cabeçalho
}
countCsvLines('./data/large.csv')
.then(n => console.log('linhas:', n))
.catch(err => console.error('erro:', err));
O que acontece depois: o arquivo é lido em blocos, readline separa por quebras de linha e o loop assíncrono processa cada linha de forma sequencial. Memória constante, sem buffers gigantes.
Limitações e quando não usar esse padrão
Este exemplo não trata CSVs com aspas, campos multi-line ou escapes. Para CSVs com complexidade, prefira bibliotecas de parsing streaming que lidam com especificação RFC4180.
Segundo exemplo mais realista
Objetivo do exemplo
Processar um CSV grande com parsing correto, inserir por lotes numa base de dados (simulado) e usar worker threads para transformação CPU-bound. Este exemplo mostra integração com stream pipeline, biblioteca de parsing e um pool simples de workers.
Setup: dependências e cenário
Vamos usar o pacote 'csv-parse' em modo streaming e 'worker_threads' do Node.js. O fluxo será:
- Readable stream do arquivo
- Parser streaming de CSV
- Transform stream que valida e enfileira para workers
- Aggregação por batch e escrita simulada
Instale: npm i csv-parse
Código principal: pipeline com parser e batch write
O bloco abaixo é o arquivo principal. Ele monta a pipeline, aceita resultados de workers e escreve por lotes. É funcional com Node.js 14+.
// O que este arquivo faz: parse streaming com csv-parse, usa workers para transformar cada linha e agrupa em batches
const fs = require('fs');
const { pipeline, Transform } = require('stream');
const parse = require('csv-parse');
const { Worker } = require('worker_threads');
const BATCH_SIZE = 1000;
function createWorkerWorker() {
return new Worker('./row-worker.js'); // veja o código do worker abaixo
}
async function processCsv(path) {
const parser = parse({ columns: true, relax_quotes: true });
let batch = [];
let activeWorkers = [];
const transformer = new Transform({
objectMode: true,
transform(record, enc, cb) {
// validações rápidas e encaminhamento para worker
if (!record.id) return cb();
const worker = createWorkerWorker();
activeWorkers.push(worker);
worker.once('message', transformed => {
batch.push(transformed);
worker.terminate();
activeWorkers = activeWorkers.filter(w => w !== worker);
if (batch.length >= BATCH_SIZE) {
// simula escrita em DB
console.log('escrevendo batch', batch.length);
batch = [];
}
cb();
});
worker.once('error', err => {
worker.terminate();
activeWorkers = activeWorkers.filter(w => w !== worker);
console.error('worker error', err);
cb(err);
});
worker.postMessage(record);
}
});
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(path);
pipeline(readStream, parser, transformer, err => {
if (err) return reject(err);
// aguardar workers ativos e gravar últimos batchs
Promise.all(activeWorkers.map(w => w.terminate())).then(() => {
if (batch.length) console.log('escrevendo último batch', batch.length);
resolve();
}).catch(reject);
});
});
}
processCsv('./data/large.csv')
.then(() => console.log('processamento concluído'))
.catch(err => console.error('falha:', err));
O que acontece depois: o parser emite objetos por linha (columns: true). Para cada record, criamos um worker que processa e retorna um objeto transformado. Em produção você não criaria um worker por linha — ver seção de melhorias abaixo.
Código do worker: transformação CPU-bound
Este é o worker que faz transformação pesada (simulada). Salve como row-worker.js no mesmo diretório.
// O que este arquivo faz: recebe um registro, faz trabalho caro e retorna um objeto transformado
const { parentPort } = require('worker_threads');
function heavyTransform(record) {
// simula CPU-bound: compressão de campos, cálculos, hashing, ...
let n = 0;
for (let i = 0; i < 200000; i++) n += i % 7;
record._checksum = (n + (record.id ? parseInt(record.id, 10) : 0)).toString(36);
return record;
}
parentPort.on('message', record => {
const out = heavyTransform(record);
parentPort.postMessage(out);
});
O que acontece depois: o worker processa e manda de volta. O main coleta e faz batch write. Com arquivos muito grandes, você deve usar um pool para limitar número de workers simultâneos.
Melhorias práticas
O exemplo acima cria muitos workers em paralelo; isso é ineficiente. Implemente um pool com tamanho fixo, reuse workers e envie tasks sequenciais por worker. Outra melhoria: use writable stream customizado que gerencia batches e backpressure corretamente.
Erros comuns de iniciante e como evitar
1. Ler tudo na memória
Muitos iniciantes usam fs.readFile para facilitar. Isso funciona para arquivos pequenos, mas não para grandes. Sempre prefira createReadStream+parser streaming para grandes volumes. Se receber uploads via HTTP, processe req como stream sem salvar o arquivo no disco.
2. Ignorar backpressure
Ignorar sinais de que o writable está lento causa acúmulo. Use pipeline, respeite retorno do .write e diga pause/resume quando necessário. O util.promisify(stream.pipeline) trata isso automaticamente para fluxos compatíveis.
3. Fazer sync no loop de leitura
Chamadas síncronas pesadas dentro do loop de processamento bloqueiam o event loop e atritam throughput. Para CPU-heavy use worker_threads ou process forks. Para IO-blocking, use operações assíncronas com limite de concorrência.
4. Criar um worker por linha
Criar um worker por registro causa overhead alto. Use um pool de workers com tamanho limitado e fila. Reutilize workers para muitas tarefas pequenas.
5. Não tratar erros e sinais
Não tratar erros de stream, workers ou do parser leva a processos presos. Sempre escute 'error' em streams e 'uncaughtException' como fallback, mas prefira design que não dependa de fallback global.
Dica prática: escreva um script separadamente para testar performance com dados sinteticos; medir é melhor que supor gargalos.
Conclusão prática
Processar CSVs grandes em Node.js é viável e robusto quando você usa streams. A estratégia correta combina leitura streaming, parser compatível com a complexidade do CSV, controle de backpressure e, quando necessário, worker threads para operações CPU-intensive. Para iniciantes: comece com readline para aprender fluxo, depois evolua para csv-parse e um pool de workers quando precisar de parsing preciso e alta performance.
Resumo rápido dos passos:
- Prefira createReadStream em vez de readFile para arquivos grandes.
- Use parsers streaming como csv-parse ou fast-csv para lidar com escapes e aspas.
- Respeite backpressure usando pipeline e streams adequados.
- Se houver CPU-bound, descarregue em worker_threads com pool.
- Teste com dados representativos e monitore memória e latência.
Pronto para o próximo passo: implemente um pool de workers, adicione métricas e substitua a simulação de escrita por gravação real em banco com transações em lote. Com essas práticas você evita estouro de memória e mantém throughput previsível.
Assuntos: Node.js, streams, csv, worker-threads, Performance
Leia também
Processar uploads CSV grandes com Streams, Worker Threads e progresso via SSE em Node.js
Processar CSV grandes sem travar o Node: mostro o jeito errado (ler tudo) e a solução correta com streams, worker threads e SSE para progresso em tempo real…
Rotação segura de refresh tokens JWT: detectar reuse e revogar sessões
Padrão prático para rotacionar refresh tokens JWT com detecção de reuse: como modelar sessões no Redis, atualizar currentJti de forma atômica, revogar sessões…
Webhooks idempotentes em Node/Express: deduplicação, verificação e retries
Guia prático para implementar webhooks idempotentes em Node/Express: validação HMAC sobre raw body, deduplicação com Redis, persistência atômica e retries com…
Abortar pipelines de Streams em Node.js: AbortController, timeouts e limpeza de Workers
Guia prático para abortar pipelines de streams em Node.js: uso de AbortController, patterns de timeout, e limpeza segura de Worker Threads para evitar…
Gerar thumbnails e checksums de uploads usando Streams e Worker Threads no Node.js
Como gerar thumbnails e checksums de uploads sem sobrecarregar o Node: padrões com Streams (PassThrough/pipeline) e offload para Worker Threads. Boas…
Diagnóstico de latência em APIs Node.js com async_hooks, event-loop e GC
Diagnostique latência em APIs Node.js combinando async_hooks/AsyncLocalStorage, monitorEventLoopDelay e análise de GC. Passo a passo prático: medir…