VOOZH about

URL: https://thenewstack.io/node-js-readable-streams-explained/

⇱ Node.js Readable Streams Explained - The New Stack


TNS
SUBSCRIBE
Join our community of software engineering leaders and aspirational developers. Always stay in-the-know by getting the most important news and exclusive content delivered fresh to your inbox to learn more about at-scale software development.
REQUIRED
It seems that you've previously unsubscribed from our newsletter in the past. Click the button below to open the re-subscribe form in a new tab. When you're done, simply close that tab and continue with this form to complete your subscription.
The New Stack does not sell your information or share it with unaffiliated third parties. By continuing, you agree to our Terms of Use and Privacy Policy.
Welcome and thank you for joining The New Stack community!
Please answer a few simple questions to help us deliver the news and resources you are interested in.
REQUIRED
REQUIRED
REQUIRED
REQUIRED
REQUIRED
Great to meet you!
Tell us a bit about your job so we can cover the topics you find most relevant.
REQUIRED
REQUIRED
REQUIRED
REQUIRED
REQUIRED
Welcome!

We’re so glad you’re here. You can expect all the best TNS content to arrive Monday through Friday to keep you on top of the news and at the top of your game.

What’s next?

Check your inbox for a confirmation email where you can adjust your preferences and even join additional groups.

Follow TNS on your favorite social media networks.

Become a TNS follower on LinkedIn.

Check out the latest featured and trending stories while you wait for your first TNS newsletter.

PREV
1 of 2
NEXT
VOXPOP
As a JavaScript developer, what non-React tools do you use most often?
Angular
0%
Astro
0%
Svelte
0%
Vue.js
0%
Other
0%
I only use React
0%
I don't use JavaScript
0%
Thanks for your opinion! Subscribe below to get the final results, published exclusively in our TNS Update newsletter:
NEW! Try Stackie AI
From clobbered drafts to real-time sync
Apr 14th 2026 10:00am, by David Moore
TypeScript 6.0 RC arrives as a bridge to a faster future
Mar 14th 2026 9:00am, by Darryl K. Taft
Mastra empowers web devs to build AI agents in TypeScript
Jan 28th 2026 11:00am, by Loraine Lawson
2021-11-24 07:55:31
Node.js Readable Streams Explained
contributed,sponsor-logdna,sponsored,sponsored-post-contributed,
Developer tools / JavaScript / Software Development

Node.js Readable Streams Explained

Implementing read streams in Node.js can be confusing. Streams are very stateful, so how they function can depend on the mode they're in.
Nov 24th, 2021 7:55am by Darin Spivey
👁 Featued image for: Node.js Readable Streams Explained
Photo by Arnie Chou from Pexels.
LogDNA sponsored this post.
“Streams are hard” …said plenty of Node.js developers before you, even experts. After working with them for a bit while writing TailFile, I discovered that they’re not really hard, there’s just a lot of moving parts. The Node.js documentation on the subject is pretty scattered — it’s a BIG feature — and really important details can sometimes just be said in a few words within one sentence in a very big document. So, this makes putting it all together pretty tough. Streams are also very stateful, so how they function does at times depend on the mode that they’re in. Hopefully I can demystify some of the confusion with this article. Note that writable streams, and even filesystem streams, have different implementations for the same concepts, so for this article, we’ll talk about implementing read streams.
Darin Spivey
Darin is a senior software engineer at LogDNA, where he works on product architecture and performance, advanced testing frameworks, and LogDNA's open source projects. When he’s not geeking out on code, you’ll find him tinkering with his smart home technology and traveling with his family.

What’s a Stream Implementation?

A readable implementation is a piece of code that extends Readable, which is the Node.js base class for read streams. It can also be a simple call to the new Readable() constructor, if you want a custom stream without defining your own class. I’m sure plenty of you have used streams from the likes of HTTP res handlers to fs.createReadStream file streams. An implementation, however, needs to respect the rules for streams, namely that certain functions are overridden when the system calls them for stream flow situations. Let’s talk about what some of this looks like.
const {Readable} = require('stream')

// This data can also come from other streams :]
let dataToStream = [
 'This is line 1\n'
, 'This is line 2\n'
, 'This is line 3\n'
]

class MyReadable extends Readable {
 constructor(opts) {
 super(opts)
 }

 _read() {
 // The consumer is ready for more data
 this.push(dataToStream.shift())
 if (!dataToStream.length) {
 this.push(null) // End the stream
 }
 }

 _destroy() {
 // Not necessary, but illustrates things to do on end
 dataToStream = null
 }
}

new MyReadable().pipe(process.stdout)
The takeaways from this are:
  • Of course, call super(opts)or nothing will work.
  • _read is required and is called automatically when new data is wanted.
  • Calling push(<some data>) will cause the data to go into an internal buffer, and it will be consumed when something, like a piped writable stream, wants it.
  • push(null) is required to properly end the read stream.
    • An 'end' event will be emitted after this.
    • A 'close' event will also be emitted unless emitClose: false was set in the constructor.
  • _destroy is optional for cleanup things. Never override destroy; always use the underscored method for this and for _read.
For such a simple implementation, there’s no need for the class. A class is more appropriate for things that are more complicated in terms of their underlying data resources, such as TailFile. This particular example can also be accomplished by constructing a Readable inline:
const {Readable} = require('stream')

// This data can also come from other streams :]
let dataToStream = [
 'This is line 1\n'
, 'This is line 2\n'
, 'This is line 3\n'
]

const myReadable = new Readable({
 read() {
 this.push(dataToStream.shift())
 if (!dataToStream.length) {
 this.push(null) // End the stream
 }
 }
, destroy() {
 dataToStream = null
 }
})

myReadable.pipe(process.stdout)

However, there’s one major problem with this code. If the data set were larger, from a file stream, for example, then this code is repeating a very common mistake with node streams: This doesn’t respect backpressure.

What’s Backpressure?

Remember the internal buffer that I mentioned above? This is an in-memory data structure that holds the streaming chunks of data — objects, strings or buffers. Its size is controlled by the highWaterMark property, and the default is 16KB of byte data, or 16 objects if the stream is in object mode. When data is pushed through the readable stream, the push method may return false. If so, that means that the highWaterMark is close to, or has been, exceeded, and that is called backpressure. If that happens, it’s up to the implementation to stop pushing data and wait for the _read call to come, signifying that the consumer is ready for more data, so push calls can resume. This is where a lot of folks fail to implement streams properly. Here are a couple of tips about pushing data through read streams:
  • It’s not necessary to wait for _read to be called to push data as long as backpressure is respected. Data can continually be pushed until backpressure is reached. If the data size isn’t very large, it’s possible that backpressure will never be reached.
  • The data from the buffer will not be consumed until the stream is in a reading mode. If data is being pushed, but there are no 'data' events and no pipe, then backpressure will certainly be reached if the data size exceeds the default buffer size.
This is an excerpt from TailFile, which reads chunks from the underlying resource until backpressure is reached or all the data is read. Upon backpressure, the stream is stored and reading is resumed when _read is called.
async _readChunks(stream) {
 for await (const chunk of stream) {
 this[kStartPos] += chunk.length
 if (!this.push(chunk)) {
 this[kStream] = stream
 this[kPollTimer] = null
 return
 }
 }
 // Chunks read successfully (no backpressure)

 return
 }

 _read() {
 if (this[kStream]) {
 this._readChunks(this[kStream])
 }
 return
 }

In Summary

There’s a lot more to it, especially when you talk about write streams, but the concepts are all the same. As I stated above, the information for streams is plentiful, but scattered. As I write this, I cannot find the place where I learned that ‘push can be called continuously’, but trust me, it’s a thing, even though the backpressure doc below always recommends waiting for _read. The fact is, depending on what you’re trying to implement, the code becomes less clear-cut, but as long as backpressure rules are followed and methods are overridden as required, then you’re on the right track!
Mezmo, formerly LogDNA, is an observability platform to manage and take action on your data. It ingests, processes, and routes log data to fuel enterprise-level application development and delivery, security, and compliance use cases.
Learn More
The latest from LogDNA

Helpful Resources

These are the documents I really learned some things from. Check them out!
Mezmo, formerly LogDNA, is an observability platform to manage and take action on your data. It ingests, processes, and routes log data to fuel enterprise-level application development and delivery, security, and compliance use cases.
Learn More
The latest from LogDNA
TRENDING STORIES
Darin is a senior software engineer at LogDNA, where he works on product architecture and performance, advanced testing frameworks, and LogDNA's open source projects. When he’s not geeking out on code, you’ll find him tinkering with his smart home technology...
Read more from Darin Spivey
LogDNA sponsored this post.
SHARE THIS STORY
TRENDING STORIES
TNS owner Insight Partners is an investor in: Pragma.
SHARE THIS STORY
TRENDING STORIES
TNS DAILY NEWSLETTER Receive a free roundup of the most recent TNS articles in your inbox each day.
The New Stack does not sell your information or share it with unaffiliated third parties. By continuing, you agree to our Terms of Use and Privacy Policy.