Streams in Node.js: Deep Dive Into Internal Working, OS Buffers, and Network Flow
Introduction
Streams in Node.js: Deep Dive Into Internal Working, OS Buffers, and Network Flow

Image Credit: DauleDK
Introduction
Streams are one of the most important performance primitives in Node.js. Many developers learn that:
“Streams are memory efficient.”
But far fewer understand why they are efficient, how they work internally, and what actually happens from your application code all the way down to the operating system and network stack.
This article explains streams deeply — from Node.js abstractions to TCP send buffers — so you understand not just how to use streams, but why they scale.
Note: This blog was originally published on my personal blog(https://ajaykrp.me). I’m publishing it here so it can reach a broader audience and help more people understand the topic better.
Mental Model First
Before diving in, hold this picture in your head:
A stream is a CONVEYOR BELT of bytes.
[producer] →→→ [chunk] [chunk] [chunk] →→→ [consumer]
↑ ↑
writable side readable side
If the consumer slows down, the belt slows down (backpressure).
If the consumer stops, the belt stops.
At no point do we pile up the entire payload in one place.
Every stream concept — pipe, backpressure, highWaterMark, drain — is just a detail of how this conveyor belt is built and controlled.
1. What Is a Stream?
A stream is an abstraction for processing data incrementally over time rather than all at once.
Instead of:
Read entire payload → Process → Send
A stream allows:
Read chunk → Process → Send
Read chunk → Process → Send
Read chunk → Process → Send
The Four Stream Types
Node.js has exactly four kinds of streams. Memorize them:
TypePurposeReal ExampleReadableSource you read FROMfs.createReadStream, reqWritableDestination you write TOfs.createWriteStream, resDuplexBoth readable AND writable, separatenet.Socket, tls.TLSSocketTransformDuplex where output is derived from inputzlib.createGzip, crypto.createCipheriv
Readable: [source] →→→ you
Writable: you →→→ [sink]
Duplex: you ←→→ [peer] (two independent channels)
Transform: in →→ [f(x)] →→ out (output is a function of input)
A Transform stream is the workhorse for pipelines: gzip, encryption, parsing, line-splitting, JSON streaming—all are transforms.
2. What Is a Buffer?
A buffer is a raw block of memory used to store binary data.
Example:
const buffer = Buffer.from("Hello");
Internally:
48 65 6c 6c 6f ← raw bytes (ASCII for H, e, l, l, o)
Buffers are needed because:
- Files are binary
- Network packets are binary
- Compression/encryption operate on bytes
- JavaScript strings are UTF-16 in V8 — networking needs raw bytes
Buffer vs String — A Subtle Trap
const s = "héllo"; // 5 chars in JS
Buffer.byteLength(s); // 6 bytes in UTF-8 (é is 2 bytes)
Streams in binary mode deal in Buffer chunks. Streams in object mode deal in arbitrary JS objects (more on that later).
Buffer ≠ Stream Internal Buffer
There are two “buffers” people confuse:
Bufferobject — a chunk of bytes you pass around.- The stream’s internal buffer — a queue of pending chunks held inside the stream. Its size limit is the
highWaterMark.
When the article says “buffer fills up,” it means #2.
3. Buffered Response vs Streamed Response
Buffered Response
const data = await fs.promises.readFile(filePath);
res.send(data);
Lifecycle
1. Read full file into memory ← peak RAM = file size
2. Allocate full buffer
3. Send entire response
Memory Picture
File on disk: [ 500 MB video.mp4 ]
RAM during req: [ 500 MB copy in process heap ] ← whole file lives here
Multiply by 100 concurrent users → 50 GB RAM. Process dies.
Streamed Response
createReadStream(filePath).pipe(res);
Lifecycle
1. Open file stream
2. Read chunk (default 64 KB)
3. Send chunk immediately
4. Wait if socket is slow (backpressure)
5. Repeat until EOF
Memory Picture
File on disk: [ 500 MB video.mp4 ]
RAM during req: [ ~64 KB rolling window ] ← only the chunk in flight
100 concurrent users → ~6.4 MB. Same hardware, 7800× the headroom.
4. Why Streaming Is Better
Streaming improves:
Memory Usage
Avoids full payload buffering. RAM stays flat regardless of payload size.
Throughput
Less allocation/copying. The GC isn’t churning through 500 MB allocations.
Latency (Time to First Byte)
The client sees bytes immediately, not after the entire file is read. TTFB drops from “seconds” to “milliseconds.”
Scalability
Handles more concurrent requests. The bottleneck shifts from RAM to network — which is what you want.
Composability
Streams plug together via .pipe() or pipeline(). You can insert gzip, encryption, rate-limiting between source and sink without rewriting either side.
5. Real Benchmark Results
Under concurrent load:
ApproachRequests/secAvg LatencyStreaming9,05810.54 msBuffering5,93516.34 ms
This demonstrates substantial throughput and latency improvements.
The deeper reason: under buffering, the event loop is blocked allocating and copying large buffers. Under streaming, the event loop ticks through tiny chunks and stays responsive.
6. How .pipe() Works Internally
When you write:
readable.pipe(writable);
Node roughly does:
readable.on('data', chunk => {
const canContinue = writable.write(chunk);
if (!canContinue) {
readable.pause(); // stop firing 'data'
}
});
writable.on('drain', () => {
readable.resume(); // resume firing 'data'
});
readable.on('end', () => {
writable.end(); // close the destination
});
.pipe() is syntactic sugar over this orchestration.
Why pipeline() Is Better Than .pipe()
.pipe() has a notorious flaw: it doesn't propagate errors or clean up correctly. If the source errors, the destination is left dangling, leaking file descriptors.
Use pipeline() (or its promise version) instead:
import { pipeline } from 'node:stream/promises';
await pipeline(
fs.createReadStream('input.txt'),
zlib.createGzip(),
fs.createWriteStream('input.txt.gz')
);
pipeline():
- Propagates errors to the final callback / awaited promise
- Destroys all streams on failure (no FD leaks)
- Handles
endcorrectly across all stages
Rule of thumb: never use .pipe() in production code. Always pipeline().
7. Backpressure Explained
Problem
What if producer is faster than consumer?
Example:
Disk read speed (NVMe ~3 GB/s) > Network send speed (1 Gbps ≈ 125 MB/s)
Without backpressure:
Chunks accumulate infinitely in RAM
Disaster. OOM kill.
A Concrete Scenario
// BAD: ignores write() return value
readable.on('data', chunk => {
writable.write(chunk); // we don't care if it returns false
});
If readable produces at 3 GB/s and writable drains at 125 MB/s, the writable's internal buffer grows by ~2.875 GB every second. Process dies in seconds.
Solution
Writable stream signals capacity using:
const ok = writable.write(chunk);
Returns:
true→ Internal buffer belowhighWaterMark. Continue writing.false→ Internal buffer at/abovehighWaterMark. Pause and wait.
Then later:
writable.on('drain', () => {
// buffer drained below highWaterMark, safe to resume
});
highWaterMark — The Knob You Should Know
Every stream has a highWaterMark:
Stream typeDefault highWaterMarkReadable (binary)64 KBWritable (binary)16 KBObject mode16 objects
This is not a hard cap — it’s the threshold at which write() starts returning false. The stream will still accept more data; it's just signalling "please slow down."
You can tune it:
fs.createReadStream('big.bin', { highWaterMark: 1024 * 1024 }); // 1 MB chunks
Bigger highWaterMark = fewer syscalls, more RAM per stream. Tune for your workload.
8. Why Backpressure Matters
Imagine 100 users downloading large files on slow internet.
Without backpressure:
100 × entire files buffered in memory
With backpressure:
Only manageable chunks buffered per connection
Visual Analogy
Picture a bartender (producer) pouring beer into a glass (buffer) being drunk by a customer (consumer):
- No backpressure: bartender keeps pouring even when the glass overflows. Beer everywhere. → OOM.
- Backpressure: bartender pours, sees glass is full, stops. Customer drinks. Glass has space. Bartender pours again. → smooth.
write() → false is the bartender noticing the glass is full. 'drain' event is the customer signalling "I have room now."
9. What res Really Is
In Express:
res
wraps Node’s:
http.ServerResponse
Which is a:
Writable Stream
That means:
res.write(chunk);
res.end();
is valid manual streaming. And req is a Readable stream — you can .pipe() it into a file or transform.
File Upload via Streaming
app.post('/upload', (req, res) => {
pipeline(
req, // Readable: incoming bytes
fs.createWriteStream('./uploaded.bin'), // Writable: disk
err => {
if (err) return res.status(500).end('upload failed');
res.end('ok');
}
);
});
The upload never lives in RAM as a whole — it streams from socket directly to disk. This is how you accept multi-GB uploads on a 256 MB container.
10. Manual Streaming Example
app.get('/manual', (req, res) => {
res.write('Chunk 1\n');
setTimeout(() => {
res.write('Chunk 2\n');
}, 1000);
setTimeout(() => {
res.write('Chunk 3\n');
res.end();
}, 2000);
});
Server-Sent Events (SSE) — Real-World Manual Streaming
app.get('/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const id = setInterval(() => {
res.write(`data: ${JSON.stringify({ ts: Date.now() })}\n\n`);
}, 1000);
req.on('close', () => clearInterval(id));
});
This connection stays open forever, pushing events. The client uses EventSource to consume. It's exactly how ChatGPT-style streaming works underneath.
AI Token Streaming — Why It Feels Fast
app.get('/chat', async (req, res) => {
res.setHeader('Content-Type', 'text/plain');
const stream = await llm.completeStreaming(req.query.prompt);
for await (const token of stream) {
res.write(token); // user sees tokens as they're generated
}
res.end();
});
The user starts reading tokens 100ms after the request — even if the full answer takes 30 seconds. Without streaming, the user stares at a spinner for 30 seconds.
11. What Happens During res.send(buffer)
res.send(buffer);
Roughly becomes:
res.end(buffer);
Internally:
socket.write(buffer);
socket.end();
But there’s a subtlety: res.send() also sets Content-Length, picks the right Content-Type, handles ETag, and may compress. It's a one-shot helper for "I have all the bytes, send them now."
12. Important Clarification
Buffered responses still use streams internally.
The difference is:
Buffered
Application prepares full payload first, then hands it to the stream.
Streamed
Application writes incrementally, letting the stream pace the work.
Buffered: [build full payload] → [stream out] ← peak memory before streaming
Streamed: [build chunk] → [stream out] → repeat ← constant memory
The socket and OS see almost identical sequences of writes. The difference is where the payload lives during construction: in your process heap (buffered) vs. trickling through (streamed).
13. What Happens at OS Level
When Node executes:
socket.write(buffer);
It does NOT send directly to network.
Instead:
Node (V8 heap Buffer)
↓ libuv copies to a uv_buf_t
↓ uv__write() calls write(2) syscall
↓ kernel copies to socket send buffer (kernel space)
↓ TCP layer takes over (segmentation, ACKs, retransmit)
↓ NIC DMA's bytes onto the wire
The application returns from socket.write() as soon as bytes are in the kernel send buffer. Network delivery is the kernel's problem after that.
The libuv Layer
Node’s stream → socket path goes through libuv:
JS: socket.write(chunk)
↓
C++: StreamWrap::WriteBuffer
↓
libuv: uv_write() → enqueues a write request
↓
libuv event loop: when fd is writable, calls write(2)
↓
kernel: copies to send buffer, returns count
If write(2) returns EAGAIN (kernel buffer full), libuv waits for the socket to become writable (via epoll/kqueue/IOCP) and retries. This is the underlying mechanism that makes socket.write() return false.
14. TCP Send Buffer
OS stores outgoing bytes temporarily:
[TCP SEND BUFFER] (kernel memory, per-socket)
chunk1
chunk2
chunk3
Why?
- CPU/memory faster than network — needs smoothing
- TCP needs unacked bytes around for retransmission
- Allows the application to write ahead without blocking on each ACK
Inspecting It on Linux
# default send/receive buffer sizes
sysctl net.ipv4.tcp_wmem # min default max for sends
sysctl net.ipv4.tcp_rmem # min default max for receives
# per-socket: ss -tmi shows skmem (sk buffer info)
ss -tmi
Typical defaults: 16 KB initial, autotuned up to 4 MB. The kernel grows the buffer based on connection bandwidth-delay product (BDP).
15. Packetization
Kernel splits outgoing bytes into packets:
Example MSS (max segment size, MTU minus headers):
MTU 1500 bytes → MSS ~1460 bytes (after IP+TCP headers)
Large write becomes:
Packet 1 (1460 bytes payload)
Packet 2 (1460 bytes payload)
Packet 3 (1460 bytes payload)
...
Critical insight: your socket.write(64KB) call does not equal one packet. The kernel will split it into ~45 segments, governed by:
- MSS (path MTU)
- Congestion window (cwnd)
- Receiver window (rwnd)
- Nagle’s algorithm (small writes coalesced)
This is why “chunk size” in your application doesn’t directly map to “packet size” on the wire.
16. Why socket.write() Returns False
If TCP send buffer fills:
socket.write(chunk) === false
Meaning:
Kernel buffer full, stop writing.
This triggers Node backpressure. The chain of events:
1. socket.write(big chunk)
2. libuv calls write(2)
3. write(2) returns fewer bytes than requested (or EAGAIN)
4. libuv buffers the rest in JS-land queue
5. Node sees JS queue >= highWaterMark
6. socket.write() returns false
7. Producer pauses
8. Kernel drains buffer to network
9. Socket becomes writable again (epoll wakes up)
10. libuv flushes queued writes
11. JS queue drops below highWaterMark
12. 'drain' event fires
13. Producer resumes
Every Node stream-to-socket pipeline is doing this dance, invisibly, billions of times per day across the internet.
17. Full End-to-End Stream Pipeline
Disk/File
↓
Kernel Page Cache / Disk Read
↓
Node Readable Stream (libuv reads fd → JS Buffer)
↓
.pipe() / pipeline()
↓
HTTP Response Writable Stream
↓
net.Socket (libuv write queue)
↓
Kernel TCP Send Buffer
↓
TCP Packetization (segmentation, cwnd, rwnd)
↓
NIC → Network
↓
Browser TCP Receive Buffer
↓
Browser HTTP Parser (handles framing: Content-Length / chunked)
↓
Frontend Consumer (fetch response.body / EventSource / etc.)
Every arrow is a place backpressure can be applied. The system is a chain of producer-consumer pairs, each with its own buffer and signalling mechanism.
18. Browser Perspective
The browser does not know “this is a stream” explicitly.
It only sees:
Bytes arriving over time.
It uses the HTTP framing rules (next section) to know where one response ends and the next begins.
19. HTTP Framing Indicators
How does a receiver know when a response is “done” if bytes just keep arriving?
Content-Length
Content-Length: 1048576
Browser knows exact size — read 1,048,576 bytes, then stop.
Used when: you know the size up front (static files, in-memory payloads).
Chunked Encoding
Transfer-Encoding: chunked
Browser knows:
Read chunks until end marker.
Used when: you don’t know the size up front (dynamic streams, SSE, AI responses).
Node automatically picks chunked encoding when you call res.write() without setting Content-Length.
20. Chunked Encoding Example
Raw HTTP:
HTTP/1.1 200 OK
Transfer-Encoding: chunked
5\r\n
Hello\r\n
6\r\n
World\r\n
4\r\n
Data\r\n
0\r\n
\r\n
Each chunk:
<size in hex>\r\n
<size bytes of payload>\r\n
Terminator: 0\r\n\r\n (a chunk of size zero).
Chunk sizes are hexadecimal. So 1000\r\n means a 4096-byte chunk, not 1000.
This framing is what lets the browser know res.write('hello') and res.write('world') are two separate pieces of an unbounded response.
21. Frontend Consumption Nuance
Even if backend streams:
Frontend may still buffer.
Example:
const data = await fetch(url).then(r => r.json());
r.json() waits for the entire body before parsing. The streaming benefits are lost on the frontend even though the server streamed perfectly.
Streaming Properly on the Frontend
const res = await fetch(url);
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
console.log('chunk:', text); // process incrementally
}
Or with async iteration:
for await (const chunk of res.body) {
// chunk is a Uint8Array
}
Or EventSource for SSE:
const es = new EventSource('/events');
es.onmessage = e => console.log(e.data);
22. True End-to-End Streaming Requires Both Sides
Backend:
- Stream response (no
Content-Lengthprecomputed, write incrementally)
Frontend:
- Consume incrementally (
reader.read(),for await,EventSource)
Example:
const reader = response.body.getReader();
If either side buffers, the chain is broken. A streamed backend behind a buffering proxy (some CDNs, some nginx configs without proxy_buffering off) loses all streaming benefits.
Common Buffering Culprits to Disable
nginx:proxy_buffering off;for streaming endpoints- Cloudflare: streaming works, but compression/buffering may need tuning
- Express middleware that calls
res.json()(buffers) - Compression middleware sometimes buffers small chunks — flush after each write
23. When To Use Streams
Use streams for:
- Large file downloads (videos, ZIPs, dataset exports)
- Video/audio serving (with
Rangerequests for seeking) - Compression pipelines (gzip on the fly)
- File uploads (avoid loading the upload into RAM)
- Proxying external responses (act as a relay, not a sponge)
- AI token streaming (LLM completions character-by-character)
- CSV exports (millions of rows, started immediately)
- Database exports (cursor-based row streaming)
- Log tailing (real-time log shipping)
- ETL pipelines (read-transform-write without staging full dataset)
24. When Streams May Not Matter Much
For tiny payloads:
1 KB JSON API response
Buffering is usually fine. The overhead of stream setup (event registration, internal queues, chunked framing) outweighs the memory savings.
Streams shine when:
- Payload large (>~100 KB)
- Transformations involved (gzip, encrypt, parse)
- Concurrency high (many simultaneous responses)
- Response generation incremental (LLMs, DB cursors, computed reports)
25. Common Pitfalls and Gotchas
Things that bite people in production:
1. Ignoring write()'s return value
// BAD
src.on('data', c => dst.write(c));
// GOOD
src.on('data', c => {
if (!dst.write(c)) src.pause();
});
dst.on('drain', () => src.resume());
// BEST
pipeline(src, dst, err => { /* ... */ });
2. Using .pipe() without error handling
pipe() does NOT forward errors. If src errors, dst is left open. Use pipeline().
3. Forgetting res.end()
The client hangs waiting for more bytes that never come. Always end() (or use pipeline() which does it for you).
4. Reading a stream twice
Streams are single-use. Once consumed, they’re done. To re-read a file, create a new ReadStream.
5. Mixing flowing and paused mode
// Adding a 'data' listener puts the stream in flowing mode.
// Calling .read() expects paused mode.
// Don't mix.
stream.on('data', ...); // flowing
stream.read(); // bug
6. for await doesn't always handle backpressure into a writable
for await (const chunk of readable) {
writable.write(chunk); // ignores backpressure!
}
Use pipeline(readable, writable) instead, or check the return value and await 'drain'.
7. Object mode confusion
const s = new Readable({ objectMode: true });
s.push({ id: 1 }); // OK
s.push('string'); // also OK
In object mode, highWaterMark counts items, not bytes. Default is 16.
8. Memory leak from unconsumed streams
If you create a Readable and never consume it, its internal buffer fills up to highWaterMark and stops. But if you've attached 'data' listeners that hold references, you can leak. Always destroy unused streams: stream.destroy().
26. Building a Custom Transform Stream
Writing your own transform is the best way to internalize streams.
import { Transform } from 'node:stream';
class UpperCase extends Transform {
_transform(chunk, encoding, callback) {
callback(null, chunk.toString().toUpperCase());
}
}
pipeline(
process.stdin,
new UpperCase(),
process.stdout
);
A Line-Splitter (a real-world useful transform)
class LineSplitter extends Transform {
constructor() {
super({ readableObjectMode: true });
this.buffer = '';
}
_transform(chunk, enc, cb) {
this.buffer += chunk.toString();
const lines = this.buffer.split('\n');
this.buffer = lines.pop(); // last partial line stays
for (const line of lines) this.push(line);
cb();
}
_flush(cb) {
if (this.buffer) this.push(this.buffer);
cb();
}
}
Now you can:
pipeline(
fs.createReadStream('huge.log'),
new LineSplitter(),
new Transform({
objectMode: true,
transform(line, enc, cb) {
if (line.includes('ERROR')) this.push(line + '\n');
cb();
}
}),
fs.createWriteStream('errors.log')
);
This processes a 100 GB log file in constant memory.
27. Async Iterators — The Modern API
Modern Node makes streams AsyncIterable. Often cleaner than events:
for await (const chunk of fs.createReadStream('data.bin')) {
// chunk is a Buffer
}
Convert any async iterable into a Readable:
import { Readable } from 'node:stream';
async function* generate() {
for (let i = 0; i < 1_000_000; i++) yield `row ${i}\n`;
}
Readable.from(generate()).pipe(res);
This is gold for streaming database cursors, paginated APIs, or generated content. You define a generator, Node handles backpressure automatically.
28. Key Engineering Takeaways
Buffered Responses
- Simpler
- Fine for small payloads (<100 KB)
- Higher memory/copy cost
- TTFB = time to build full payload
Streamed Responses
- Better scalability
- Lower memory footprint (constant w.r.t. payload size)
- Lower perceived latency (TTFB = time to first chunk)
- Backpressure-aware
- Composable via
pipeline()
Hard Rules
- Always use
pipeline()over.pipe()in production - Always check
write()'s return value if writing manually - Always handle
errorevents — unhandled stream errors crash the process - Always call
destroy()on streams you abandon
29. Interview-Ready Summary
“Node.js streams enable incremental, backpressure-aware data flow through readable, writable, duplex, and transform abstractions. In HTTP responses, streamed delivery allows the server to begin transmitting before full payload generation, reducing memory pressure and improving scalability. Internally,
socket.write()enqueues bytes into libuv's write queue, which callswrite(2)to copy data into the kernel's TCP send buffer. When that buffer fills, the syscall returns EAGAIN, libuv parks the write, andwrite()returnsfalseto JavaScript — propagating backpressure all the way back to the source. The kernel then handles segmentation according to MSS, congestion window, and receiver window, before the NIC puts bytes on the wire. The receiver reassembles the stream and the application uses HTTP framing (Content-Length or chunked encoding) to know where the response ends."
30. Quick Reference Cheatsheet
TYPES
Readable fs.createReadStream, http req
Writable fs.createWriteStream, http res
Duplex net.Socket
Transform zlib.createGzip, crypto.createCipheriv
KEY METHODS
readable.pipe(writable) ← legacy, no error handling
pipeline(...streams, callback) ← USE THIS
readable.read([size]) ← paused mode
writable.write(chunk) → bool ← false = pause
writable.end([chunk])
KEY EVENTS
'data', 'end', 'error', 'close' (Readable)
'drain', 'finish', 'error', 'close' (Writable)
KEY CONFIG
highWaterMark: 64KB readable, 16KB writable, 16 objects
BACKPRESSURE PROTOCOL
write() returns false → pause source
'drain' event → resume source
OS LAYER
socket.write → libuv → write(2) → kernel send buffer → TCP segments → NIC
FRAMING
Content-Length: <n> ← known size
Transfer-Encoding: chunked ← unknown size, hex-prefixed chunks, 0\r\n\r\n ends
Final Thoughts
Streams are not magic.
They are a coordinated pipeline of:
Application Logic
→ Node Stream Abstractions (Readable/Writable/Duplex/Transform)
→ libuv Write Queue (event loop integration)
→ OS Socket Buffers (kernel send/receive buffers)
→ TCP Flow Control (cwnd, rwnd, ACKs)
→ Network Transport (IP packets, MTU)
→ Receiver Buffers (kernel + application)
→ Client Parsing (HTTP framing)
Each layer applies backpressure to the layer above it. The system is fractal: the same producer-consumer-with-buffer pattern repeats at every level, from your for await loop down to TCP's sliding window.
Understanding this pipeline is what allows you to:
- Build scalable APIs
- Debug latency issues (where is the buffer that’s filling?)
- Optimize throughput (which
highWaterMarkis the bottleneck?) - Design robust streaming systems (where can errors propagate?)
Once you understand streams at this level, many backend performance concepts — event loops, GC pressure, TCP tuning, async iteration, pub/sub systems — start fitting into the same mental model. Streams are the gateway drug to systems thinking.
메타데이터
- post_id
- 9edd95a3b08c
- slug
- streams-in-node-js-deep-dive-into-internal-working-os-buffers-and-network-flow-9edd95a3b08c
- url
- https://blog.devgenius.io/streams-in-node-js-deep-dive-into-internal-working-os-buffers-and-network-flow-9edd95a3b08c
- canonical_url
- https://blog.devgenius.io/streams-in-node-js-deep-dive-into-internal-working-os-buffers-and-network-flow-9edd95a3b08c
- author_url
- https://medium.com/@krp-ajay
- status
- ok
- fetched_at
- 2026-07-10 15:36:45