Readable tem 2 modos: flowing (listener de 'data' → chunks empurrados automaticamente) e paused (.read() puxa chunk a chunk; default em Node moderno). Eventos principais: data, end, error, close, readable. Implementação custom: subclasse Readable + _read(size) + push(chunk) / push(null) (sinal de fim). Readable.from(iterable) é o atalho moderno para criar Readable em produção sem subclasse. Em 2026, o idioma canônico de consumo é for await...of — veja [[08 - Async iteration de streams]].
O que é
Readable é a classe base de Node.js para fontes de dados. Qualquer coisa que produz bytes ou objetos para consumo downstream é uma Readable: fs.createReadStream, HTTP response no lado do cliente, process.stdin, resultado de zlib.createGunzip() lido após inflate.
A classe vive em node:stream e pode ser:
Usada diretamente via Readable.from(iterable) — o caminho moderno.
Subclassificada para implementar fontes customizadas via _read(size).
Consumida via eventos ('data', 'readable'), .pipe(), ou for await...of.
A propriedade readable.readableFlowing expõe o estado interno como três valores possíveis:
Valor
Significado
null
Nenhum mecanismo de consumo ainda; stream não gera dados
false
Pausado explicitamente (backpressure ou .pause())
true
Em flowing; emitindo dados ativamente
Diagrama: estados e transições de um Readable
stateDiagram-v2
[*] --> Null : stream criado
Null --> Flowing : on('data') / resume() / pipe()
Null --> Paused : pause() / unpipe()
Flowing --> Paused : pause()
Paused --> Flowing : resume() / on('data')
Flowing --> Ended : push(null) + buffer drenado
Paused --> Ended : push(null) + buffer drenado
Ended --> [*]
state Flowing {
direction LR
chunks : chunks empurrados via evento 'data'
}
state Paused {
direction LR
pull : consumidor chama .read() explicitamente
}
O estado null é o ponto de partida: o stream existe mas não produz dados até que um mecanismo de consumo seja ativado. A transição para Flowing acontece implicitamente ao registrar on('data') — o runtime começa a empurrar chunks automaticamente. Em Paused, o controle é do consumidor: ele puxa chunks via .read() quando está pronto.
Por que importa
Escolher o modo errado de consumo introduz bugs que não aparecem em desenvolvimento mas explodem em produção:
Perda de chunks: stream entra em flowing antes do listener estar registrado.
Vazamento de memória: stream em paused sem ninguém chamar .read() enche o buffer interno indefinidamente.
Deadlock silencioso: 'end' nunca dispara porque o dado nunca é consumido.
Backpressure ignorado: produtor mais rápido que consumidor sem controle de fluxo.
Entender os dois modos e quando cada um aplica é pré-requisito para trabalhar com qualquer stream em Node — inclusive abstrações de alto nível como pipe() e for await...of usam esses mecanismos internamente.
Como funciona
Estado interno e a transição de modos
Toda Readable começa em readableFlowing === null. O stream não produz dados enquanto nenhum mecanismo de consumo estiver ativo. A transição acontece assim:
Adicionar um listener de 'data' é suficiente para colocar o stream em flowing. Os chunks chegam empurrados pelo runtime assim que estão disponíveis no buffer interno:
import { createReadStream } from 'node:fs';const readable = createReadStream('./arquivo.txt', { encoding: 'utf8' });readable.on('data', (chunk) => { // chunk chega empurrado; você não controla o ritmo process(chunk);});readable.on('end', () => { // Disparado UMA vez, depois que o último chunk foi consumido console.log('Leitura concluída');});readable.on('error', (err) => { // SEMPRE trate 'error' — não tratar é fatal em Node 22+ console.error('Erro na leitura:', err);});
Regra: sempre registre 'error'
Em Node 22+, um 'error' sem listener lança uma exceção não capturada e derruba o processo. Não existe Readable “segura” sem handler de erro.
Paused mode — evento 'readable' + .read()
No modo paused, o runtime avisa via 'readable' que há dados disponíveis no buffer interno. Você puxa com .read(size) até receber null (buffer vazio):
import { createReadStream } from 'node:fs';const readable = createReadStream('./arquivo.txt');readable.on('readable', () => { let chunk; // Loop de pull: puxa até esvaziar o buffer while ((chunk = readable.read()) !== null) { process(chunk); }});readable.on('end', () => { console.log('Fim do stream');});readable.on('error', (err) => { console.error(err);});
.read(size) aceita um argumento opcional que especifica quantos bytes retornar. Se o buffer interno tiver menos do que size bytes, retorna null mesmo que ainda hajam dados a vir — o próximo 'readable' chegará quando o buffer atingir size.
'readable' vs 'data'
Quando ambos os listeners existem no mesmo stream, 'readable' assume controle do fluxo e 'data' só emite quando .read() é chamado. Não misture os dois estilos na mesma instância.
Implementação custom: subclasse + _read
Para criar uma fonte de dados que não existe como arquivo ou socket — gerador de sequências, paginação de API, dados sintéticos — você subclassifica Readable e implementa _read(size):
import { Readable } from 'node:stream';class CounterReadable extends Readable { constructor(max, options = {}) { super(options); this.i = 0; this.max = max; } _read(/* size é um hint; pode ser ignorado */) { if (this.i >= this.max) { // push(null) sinaliza fim do stream — OBRIGATÓRIO this.push(null); } else { // push retorna false quando o buffer está cheio (backpressure) // _read não será chamado novamente até o consumidor drenar this.push(`${this.i++}\n`); } }}// Uso:const counter = new CounterReadable(5);counter.on('data', (chunk) => process.stdout.write(chunk));counter.on('end', () => console.log('FIM'));// Saída: 0, 1, 2, 3, 4, FIM
Contrato de _read:
É chamado pelo runtime quando o consumidor precisa de mais dados (buffer abaixo de highWaterMark).
Deve chamar this.push(chunk) uma ou mais vezes, OU this.push(null) para sinalizar fim.
Não deve chamar this.push() depois de já ter feito this.push(null) — o stream estará encerrado.
O argumento size é um hint (quantos bytes o consumidor quer); pode ser ignorado com segurança.
Readable.from(iterable) — o atalho moderno
Em código de produção, criar uma subclasse raramente é necessário. Readable.from() converte qualquer iterable ou async iterable em Readable:
import { Readable } from 'node:stream';// A partir de um arrayconst fromArray = Readable.from(['chunk1\n', 'chunk2\n', 'chunk3\n']);// A partir de um generator síncronofunction* counter(max) { for (let i = 0; i < max; i++) yield `${i}\n`;}const fromGenerator = Readable.from(counter(5));// A partir de um async generator — o caso mais poderosoasync function* fetchPages(baseUrl, pages) { for (let page = 1; page <= pages; page++) { const response = await fetch(`${baseUrl}?page=${page}`); yield await response.text(); }}const fromAsyncGen = Readable.from(fetchPages('https://api.example.com/data', 3));// Consumo com for await...of (idioma canônico em 2026)for await (const chunk of fromAsyncGen) { console.log(chunk);}
Readable.from() sempre cria um stream em object mode por padrão quando o iterable produz valores que não são strings ou Buffers. Para forçar binary mode, passe { objectMode: false, encoding: 'utf8' }.
Object mode
Por padrão, Readable streams processam Buffer, string, TypedArray e DataView. Ativar objectMode: true permite qualquer valor JavaScript — objetos, números, null não é mais sinal de EOF (EOF é apenas push(null) internamente; o objeto null é bloqueado):
import { Readable } from 'node:stream';const objectStream = new Readable({ objectMode: true, read() {}});objectStream.push({ id: 1, nome: 'Alice' });objectStream.push({ id: 2, nome: 'Bob' });objectStream.push(null); // fimobjectStream.on('data', (obj) => { // obj é um plain object, não Buffer console.log(obj.nome);});
Object mode é irreversível
Não é possível alternar objectMode de uma instância existente. Defina na construção. Streams em object mode têm highWaterMark contado em número de objetos, não bytes.
Na prática
Padrão observado em codebases Node modernas (2024–2026):
Cenário
Abordagem recomendada
Criar Readable de lista/generator
Readable.from(iterable)
Consumir qualquer Readable
for await...of (veja nota 08)
Pipe para Writable/Transform
.pipe() ou stream.pipeline()
Implementar fonte custom rara
Subclasse + _read
Modo paused com .read()
Apenas em libs internas de baixo nível
Readable.from(iterable) é o default. Você raramente precisa de subclasse — bibliotecas maduras (drivers de banco, HTTP clients) já expõem Readable prontas. Subclasse aparece em entrevistas como teste de conhecimento do contrato interno, não como padrão de produção.
Modo paused com .read() está presente em código legado e em implementações de parsers de protocolo. Em código de aplicação, for await...of oferece o mesmo controle de pull com sintaxe muito mais clara.
Casos práticos
Os cenários abaixo mostram Readable em contextos de produção onde a escolha de modo e de estratégia de criação impacta diretamente na corretude do código.
Cenário 1 — Paginação de API como Readable com async generator
Um serviço de relatórios precisa processar todos os pedidos de um cliente, que podem ser milhares. A API upstream é paginada. Readable.from() com um async function* cria um stream que pagina automaticamente sob demanda:
import { Readable } from 'node:stream';import { pipeline } from 'node:stream/promises';import { Transform } from 'node:stream';import { createWriteStream } from 'node:fs';// async generator: pagina a API e faz yield de cada registro individualmenteasync function* fetchAllOrders(clientId) { let page = 1; let hasMore = true; while (hasMore) { const res = await fetch( `https://api.exemplo.com/orders?client=${clientId}&page=${page}&limit=100`, { headers: { Authorization: `Bearer ${process.env.API_TOKEN}` } } ); const { orders, pagination } = await res.json(); for (const order of orders) { yield order; // cada registro vira um chunk no stream } hasMore = pagination.hasNextPage; page++; }}// Transform: enriquece cada pedido com campo calculadoconst enrich = new Transform({ objectMode: true, transform(order, _enc, cb) { cb(null, { ...order, totalComDesconto: order.total * (1 - (order.discountPct / 100)), exportadoEm: new Date().toISOString(), }); },});// Transform: serializa objetos para NDJSONconst toNDJSON = new Transform({ objectMode: true, transform(obj, _enc, cb) { cb(null, JSON.stringify(obj) + '\n'); },});// pipeline completo — backpressure gerenciado automaticamente// A API só é chamada quando o consumidor está pronto para mais dadosawait pipeline( Readable.from(fetchAllOrders('cliente-42')), // fonte paginada enrich, // enriquecimento toNDJSON, // serialização createWriteStream('./pedidos-cliente-42.ndjson'),);console.log('Exportação concluída — memória constante durante todo o processo');
O detalhe essencial: o async function* só avança para a próxima página quando o pipeline drena o buffer interno — backpressure automático que evita fazer 100 chamadas de API antes de processar qualquer resultado.
Cenário 2 — Readable custom para leitura de banco de dados com cursor
Um job de migração precisa processar todos os registros de uma tabela de 5 milhões de linhas. O driver do banco expõe um cursor — uma API assíncrona de pull. Implementar _read() mapeia esse cursor para o ecossistema de streams:
import { Readable } from 'node:stream';import { pipeline } from 'node:stream/promises';import { createWriteStream } from 'node:fs';class DatabaseCursorReadable extends Readable { constructor(cursor, options = {}) { super({ ...options, objectMode: true }); this.cursor = cursor; this._reading = false; } _read() { // _read é chamado pelo runtime quando o buffer precisa de mais dados // Evita chamadas concorrentes ao cursor if (this._reading) return; this._reading = true; this.cursor.next() .then((row) => { this._reading = false; if (row === null) { // Sinaliza fim do stream — OBRIGATÓRIO para que 'end' dispare this.push(null); } else { // push() retorna false se o buffer está cheio (backpressure) // O runtime não chamará _read() novamente até o buffer drenar this.push(row); } }) .catch((err) => { this._reading = false; // Destrói o stream com o erro — propaga via evento 'error' this.destroy(err); }); }}// Uso: cursor do banco → Readable → arquivo de migraçãoasync function exportTable(db, tableName, outputPath) { const cursor = await db.query(`SELECT * FROM ${tableName}`).cursor(); await pipeline( new DatabaseCursorReadable(cursor), new Transform({ objectMode: true, transform(row, _enc, cb) { cb(null, JSON.stringify(row) + '\n'); }, }), createWriteStream(outputPath), ); await cursor.close();}await exportTable(db, 'users', './users-export.ndjson');
O contrato de _read() garante que o banco de dados só é consultado quando o pipeline está pronto para consumir — se a escrita em disco está lenta, o cursor pausa automaticamente. Isso evita carregar milhões de registros em memória de uma vez.
Armadilhas comuns
1. Adicionar listener 'data' tarde demais
Se o stream já entrou em flowing (por exemplo, via .resume() ou .pipe()), adicionar on('data') depois que dados começaram a fluir perde os chunks anteriores. Sempre registre listeners antes de qualquer operação que ative o stream.
// ERRADO — possível perda de dadosconst r = createReadStream('./big.txt');r.resume(); // stream começa a fluirsetTimeout(() => { r.on('data', (chunk) => process(chunk)); // chunks iniciais já foram perdidos}, 100);// CORRETO — listener antes de qualquer ativaçãoconst r = createReadStream('./big.txt');r.on('data', (chunk) => process(chunk)); // registra ANTES de fluir
2. Não tratar 'error'
Em Node 22+, um 'error' sem listener é uma uncaughtException que derruba o processo. Sempre registre on('error') em toda Readable que você consume diretamente.
3. Esquecer push(null) em _read
Se _read nunca chama push(null), o stream nunca emite 'end'. O consumidor fica bloqueado indefinidamente esperando mais dados. Isso é especialmente sutil quando push(null) está atrás de uma branch condicional que nunca é atingida.
// BUG — 'end' nunca dispara se count > max_read() { if (this.count < this.max) { this.push(`${this.count++}`); } // esqueceu: else { this.push(null); }}
4. _read síncrono que sempre tem dados sem yield
Se _read chamar push() de forma completamente síncrona e nunca receber backpressure (porque o consumidor é rápido), o runtime chama _read novamente no mesmo tick em um loop. Em generators ou fontes infinitas, isso pode esgotar a stack ou monopolizar o event loop. Use process.nextTick() ou setImmediate() para yield:
_read() { // Versão com yield para não bloquear o event loop process.nextTick(() => { this.push(`${this.i++}\n`); });}
5. Misturar estilos de consumo
Usar on('data'), on('readable'), .pipe() e for await...of no mesmo stream leva a comportamento imprevisível. Escolha um mecanismo de consumo por instância.
Em entrevista
Frase pronta:
“A Readable stream is a source of chunks. It has two modes: flowing, where you attach a 'data' listener and chunks are pushed to you; and paused, where you call .read() to pull. Modern Node code rarely uses either directly — for await...of is the canonical idiom in 2026, and it handles backpressure automatically. To create a Readable from an iterable, Readable.from() is the one-liner. To implement a custom Readable, subclass and define _read(size), calling push(chunk) for each chunk and push(null) to signal end.”
Vocabulário para usar em inglês:
PT-BR
EN
modo flowing
flowing mode
modo paused
paused mode
empurrar chunk
push a chunk
puxar chunk
pull a chunk
modo objeto
object mode
sinalizar fim
signal end-of-stream / EOF
limite de buffer
highWaterMark
contrapressão
backpressure
Perguntas frequentes e respostas curtas:
“O que acontece se você não chamar push(null)?” → O stream nunca emite 'end'; consumidores ficam bloqueados.
“Qual a diferença entre 'readable' e 'data'?” → 'data' = flowing (push); 'readable' = paused (pull). Não misture os dois.
“Quando usar subclasse vs Readable.from()?” → Readable.from() para qualquer coisa baseada em iterable/generator. Subclasse apenas quando você precisa controlar highWaterMark manualmente ou integrar com uma fonte de I/O de baixo nível.
“readableFlowing === null vs false?” → null = nenhum mecanismo de consumo ainda (stream não gera dados); false = pausado explicitamente após ter tido consumo.
O que vem a seguir
Com os modos de Readable claros, a próxima peça é entender o outro lado do fluxo: Writable streams. Enquanto Readable produz chunks, Writable os consome — e tem seu próprio conjunto de contratos críticos, incluindo o mecanismo de backpressure via retorno de .write() e o evento 'drain'.