Un stream es la respuesta de Node a una pregunta que todo programa intensivo en E/S eventualmente enfrenta: ¿qué haces cuando los datos son demasiado grandes, llegan demasiado lento o son demasiado abiertos para mantenerlos razonablemente en memoria a la vez? En lugar de devolver un valor de una sola vez, un stream te entrega datos como una secuencia de piezas más pequeñas a lo largo del tiempo, a medida que están disponibles.
Conceptos básicos de Streams muestra código funcional para los cuatro tipos de stream, y Contrapresión, Patrones Readable y Writable, y Transform y Duplex profundizan en una parte del panorama. Esta página es el marco que los engloba a todos: por qué existen los streams, cómo cooperan las piezas y el mecanismo de retroalimentación que hace que todo el sistema sea seguro bajo carga.
Un stream es una abstracción sobre datos que llegan en fragmentos a lo largo del tiempo, permitiendo que el código procese entradas arbitrariamente grandes o lentas con un uso de memoria limitado y predecible.
Por qué es importante: Cargar un archivo completo, el cuerpo de una respuesta o una carga en memoria antes de procesarlo no escala; los streams permiten que un servicio maneje cargas útiles mucho más grandes que la RAM disponible y comience a producir resultados antes de que la entrada termine de llegar.
Cuándo usarlo: Leer o escribir archivos grandes, proxy de cuerpos de solicitud/respuesta HTTP, transformar datos en tránsito (compresión, análisis, cifrado) o conectar dos puntos finales de E/S sin almacenar todo en búfer entre ellos.
Limitaciones / Compromisos: Los streams sacrifican la simplicidad por la eficiencia: el manejo de errores, la limpieza y la contrapresión requieren un manejo deliberado que una sola llamada a await readFile() nunca te pide.
Temas relacionados: Buffers, el bucle de eventos de Node.js, stream/promises pipeline, streaming de respuestas HTTP.
Antes de los streams, la forma obvia de manejar un archivo o una carga de red en Node era leer todo en memoria y luego operar sobre el resultado completo, simple de entender, pero solo viable si los datos son pequeños y finitos.
Las API principales de Node (http, fs, net, zlib y más) se basan en una idea diferente: exponer los datos como una secuencia de fragmentos discretos, cada uno un Buffer, una cadena o (en modo objeto) un valor JavaScript arbitrario, entregados a medida que están listos en lugar de todos a la vez. Un stream es el objeto que gestiona esa secuencia, produciendo fragmentos (un Readable), consumiéndolos (un Writable) o ambos.
Una analogía útil: piensa en un stream como una cinta transportadora entre dos estaciones de trabajo en lugar de una sola caja entregada por una carretilla elevadora. La cinta mueve los elementos uno a la vez, la estación receptora puede trabajar en cada elemento a medida que llega en lugar de esperar el envío completo, y, fundamentalmente, se le puede decir a la cinta que disminuya la velocidad o se detenga si la estación receptora se retrasa, en lugar de apilar cajas en el suelo.
Node define cuatro tipos de stream, cada uno un rol en esa cinta:
Readable -> produce fragmentos (ej. fs.createReadStream)
Writable -> consume fragmentos (ej. fs.createWriteStream)
Duplex -> ambos, independientemente (ej. un socket TCP)
Transform -> ambos, con salida derivada (ej. zlib.createGzip)
de la entrada (un subtipo de Duplex)
Cada tipo de stream se basa en EventEmitter: un Readable emite 'data' y 'end', un Writable emite 'drain' y 'finish', y cada stream puede emitir 'error'. Esa base compartida es la razón por la que los streams se componen de la forma en que lo hacen: conectar streams es realmente conectar productores de eventos con consumidores de eventos, con las clases de stream gestionando la contabilidad (búferes internos, máquinas de estado) además.
La pieza más importante de esa contabilidad es la contrapresión. Cada Writable tiene un búfer interno con un highWaterMark configurable (un límite suave de bytes o recuento de objetos); cuando un productor escribe más rápido de lo que un consumidor puede vaciar ese búfer, writable.write() comienza a devolver false como señal para pausar. Una API de stream que ignora esta señal y sigue escribiendo de todos modos anula todo el propósito de limitar la memoria al usar un stream en primer lugar; el búfer interno simplemente crece sin límite. Contrapresión cubre el evento drain y los mecanismos manuales de pausa/reanudación en su totalidad.
pipe() (y su contraparte moderna más segura, pipeline() de stream/promises) existe específicamente para que rara vez tengas que gestionar ese bucle de retroalimentación manualmente: conectar un Readable a un Writable conecta el manejo de la contrapresión, reenvía los fragmentos 'data' y (en el caso de pipeline()) propaga errores y garantiza la limpieza si alguna de las partes falla.
import { pipeline } from 'node:stream/promises';import { createReadStream, createWriteStream } from 'node:fs';import { createGzip } from 'node:zlib';// Cada etapa solo mantiene una ventana limitada de datos en memoria -// no el archivo completo - y la contrapresión de la etapa de escritura// ralentiza automáticamente la etapa de lectura si la E/S del disco se retrasa.await pipeline( createReadStream('input.log'), createGzip(), createWriteStream('input.log.gz'),);
Ese fragmento también demuestra por qué los streams Transform importan como su propia categoría: createGzip() es simultáneamente un Writable (aceptando bytes sin procesar) y un Readable (emitiendo bytes comprimidos), con su salida derivada causalmente de su entrada, una forma lo suficientemente distinta de un Duplex general (cuyos lados de lectura y escritura son independientes, como las dos direcciones de un socket TCP) que Node lo modela como su propia subclase. Transform y Duplex cubre la construcción de versiones personalizadas de ambos.
Los streams interactúan directamente con el modelo de E/S del bucle de eventos: un Readable obtenido de un archivo o socket no sondea los datos, sino que se basa en el mecanismo subyacente de finalización de E/S de libuv para entregar fragmentos a medida que el sistema operativo los pone a disposición, lo que es parte de la razón por la que la E/S de streaming escala bien bajo el diseño de un solo hilo y no bloqueante de Node en lugar de luchar contra él.
El manejo de errores es el punto operativo más delicado de todo este modelo. Debido a que un pipeline es en realidad varios EventEmitters que emiten de forma independiente y que están conectados, un 'error' no manejado en cualquiera de ellos bloquea el proceso por defecto, y una cadena pipe() ingenua no destruye automáticamente todos los demás streams de la cadena cuando un enlace falla, lo que puede provocar la fuga de descriptores de archivo o sockets abiertos. pipeline() de stream/promises se construyó específicamente para cerrar esa brecha: destruye todos los streams de la cadena ante cualquier fallo y devuelve una Promesa rechazada en lugar de eventos dispersos que escuchar. stream/promises Pipeline cubre esto en profundidad, y Mejores prácticas de Streams lo convierte en reglas concretas.
HTTP es uno de los lugares de mayor aprovechamiento donde este modelo aparece en la práctica: tanto la solicitud entrante como la respuesta saliente en el módulo http de Node son streams, lo que significa que un proxy o un servicio adyacente a un proxy inverso puede reenviar un cuerpo de solicitud o respuesta grande sin almacenar todo en búfer, Streaming de respuestas HTTP cubre los detalles de temporización de encabezados y codificación de transferencia por fragmentos que conlleva.
Enfoque
Fortaleza
Debilidad
Mejor ajuste
Almacenar en búfer la carga útil completa (readFile, middleware req.body)
Código simple, de sensación síncrona; fácil de entender
La memoria escala con el tamaño de la carga útil; no hay progreso hasta que se carga completamente
Cargas útiles pequeñas y limitadas (archivos de configuración, cuerpos JSON pequeños)
Cadenas pipe() manuales
Sin dependencia adicional; control directo
El manejo de errores y la limpieza en toda la cadena son responsabilidad del llamador
Reenvío simple de una sola etapa con oyentes de errores cuidadosos
stream/promisespipeline()
Limpieza automática y propagación de errores en cada etapa
Ligeramente más formalidad que una llamada pipe() simple
Cualquier pipeline de varias etapas o de producción
Iteración asíncrona (for await...of un Readable)
Se lee naturalmente como código secuencial; se integra con async/await
Solo cubre el lado del consumo; aún necesita cuidado al componer múltiples etapas
"Los streams son solo una forma más lenta de obtener los mismos datos que readFile." No se trata de velocidad para una sola lectura, sino de limitar la memoria y permitir que el procesamiento comience antes de que la entrada esté completamente disponible, lo que importa más exactamente cuando las cargas útiles son grandes o de duración indefinida.
"pipe() maneja los errores por ti." Reenvía datos y gestiona la contrapresión, pero un error en un stream en una cadena pipe() no destruye automáticamente los demás; pipeline() se construyó para cerrar esa brecha específica.
"La contrapresión es algo en lo que solo necesitas pensar para archivos enormes." Cualquier desajuste en la velocidad del productor/consumidor la activa: una Transformación rápida en memoria que alimenta un Writable de red lento experimenta la misma mecánica de highWaterMark que una copia de archivo de varios gigabytes.
"Los streams en modo objeto son una característica de nicho." Son el mecanismo detrás de patrones comunes como el envío de filas de base de datos analizadas o registros JSON delimitados por nueva línea a través de un pipeline de procesamiento; el modo objeto simplemente significa que el stream transporta valores JS completos en lugar de bytes.
"Un stream Transform es básicamente un stream Duplex con un nombre diferente." Los lados de lectura y escritura de un Duplex son independientes (como las dos direcciones de un socket); la salida de un Transform se deriva causalmente de su entrada por diseño, lo que es un contrato significativamente diferente, no una elección de nombre.
Permiten que el código procese datos que son demasiado grandes, llegan demasiado lento o son demasiado abiertos para mantenerlos razonablemente en memoria, entregándolos en fragmentos limitados a lo largo del tiempo en lugar de como un valor completo.
¿Cuáles son los cuatro tipos de stream, en una línea cada uno?
Readable - produce una secuencia de fragmentos (un archivo que se está leyendo, una solicitud HTTP entrante)
Writable - consume una secuencia de fragmentos (un archivo que se está escribiendo, una respuesta HTTP saliente)
Duplex - ambos lados de forma independiente (un socket TCP)
Transform - ambos lados, con la salida derivada de la entrada (compresión gzip, un analizador)
¿Cómo se relacionan los streams con EventEmitter?
Cada clase de stream se basa en EventEmitter: los Readables emiten 'data'/'end', los Writables emiten 'drain'/'finish', y todos los streams pueden emitir 'error'; las clases de stream añaden lógica de búfer y máquina de estado además de esa base de eventos compartida.
¿Qué es exactamente la contrapresión?
Es la señal de retroalimentación que da un Writable cuando su búfer interno está lleno: write() devuelve false, indicando al productor que pause hasta que un evento 'drain' diga que es seguro reanudar, lo que evita que un productor rápido aumente la memoria sin límite al escribir en un consumidor más lento.
¿Por qué usar `pipeline()` en lugar de `pipe()`?
pipeline() (de node:stream/promises) destruye cada stream en una cadena de varias etapas si alguno de ellos produce un error, y resuelve o rechaza una única Promesa para toda la operación; una cadena pipe() simple no propaga errores ni limpia otras etapas automáticamente, lo que puede provocar la fuga de manejadores abiertos.
¿Los streams solo funcionan con datos binarios?
No, el modo objeto permite que un stream transporte valores JavaScript arbitrarios en lugar de bytes o cadenas, que es como funcionan patrones como el streaming de filas analizadas o registros JSON a través de un pipeline de procesamiento; highWaterMark entonces cuenta objetos en lugar de bytes.
¿Está bien almacenar en búfer una carga útil completa en lugar de transmitirla?
Sí, para datos pequeños y limitados (un archivo de configuración, un cuerpo JSON pequeño) almacenar todo en búfer es más simple y el costo de memoria es insignificante; el streaming justifica su complejidad específicamente cuando el tamaño de la carga útil es grande o desconocido de antemano.
¿Cómo interactúan los streams con el bucle de eventos?
Un stream obtenido de un archivo o socket se basa en el mecanismo de finalización de E/S de libuv para entregar fragmentos a medida que el sistema operativo los pone a disposición, en lugar de sondear, por lo que la E/S de streaming se ajusta al modelo no bloqueante de Node en lugar de ir en su contra.
¿Por qué un error de stream no manejado bloquea todo el proceso?
Porque 'error' es un evento especial de EventEmitter: si no se registra ningún oyente para él, Node lo trata como una excepción no capturada y termina el proceso por defecto, por lo que cada stream en una cadena necesita manejo de errores, no solo el primero.
¿Cuál es la diferencia entre un stream Duplex y un stream Transform?
Un Duplex tiene dos lados independientes: lo que escribes y lo que lees no están relacionados (un socket TCP, por ejemplo). Un Transform es un Duplex especializado donde la salida se deriva directamente de la entrada, como un compresor gzip que convierte bytes sin procesar en bytes comprimidos.
¿Puedo consumir un stream Readable con `async`/`await` en lugar de eventos?
Sí, cualquier Readable es asíncrono-iterable, por lo que for await (const chunk of readable) funciona y se lee naturalmente como código secuencial, mientras que el stream sigue entregando fragmentos (y respeta la contrapresión) bajo el capó exactamente como lo haría con oyentes de eventos sin procesar.