When Does Node.js’s drain Event Fire? A Deep Dive into Stream Backpressure

This article investigates the conditions under which Node.js’s drain event is emitted, explores its relationship with socket.write’s return value and highWaterMark, demonstrates experiments with TCP and HTTP streams, and shows how to implement proper back‑pressure handling using pause/resume or pipe.

Node Underground
Node Underground
Node Underground
When Does Node.js’s drain Event Fire? A Deep Dive into Stream Backpressure

Background

While writing network‑request code with Node.js, the author repeatedly saw the drain event in open‑source projects and added it to his own code.

socket.on('drain', function(){ console.log('drain event fired.'); });
socket.write('some data.');

In production the drain event sometimes fired, prompting the question: when does it fire and what can it be used for?

Investigation

The Node.js documentation for socket.write states that it returns true when all data is flushed to the kernel buffer and false when data is queued in user memory; a drain event is emitted when the buffer becomes free again.

Returns true if the entire data was flushed successfully to the kernel buffer. Returns false if all or part of the data was queued in user memory. ‘drain’ will be emitted when the buffer is again free.

An initial experiment that logged the return value of socket.write never triggered drain because the written data was too small. The author then increased the traffic by creating many TCP connections, but still saw no drain events, eventually hitting “Too many open files” errors.

Looking at the Node.js source, Stream.write sets state.needDrain = true when state.length > state.highWaterMark, causing drain to be emitted later. The default highWaterMark is 16 KB.

var ret = state.length < state.highWaterMark;
if (!ret) state.needDrain = true;
return ret;

To provoke the condition, the author built an HTTP server that streams a 5 MB MP3 file and launched 1 000 concurrent GET requests. This time many drain events appeared and socket.write frequently returned false.

Because the file is read from disk faster than it can be sent over the network, the readable stream buffers data, eventually filling the writable stream’s internal buffer.

var http = require('http');
var fs = require('fs');
http.createServer(function(req, res) {
  var filePath = './Christmas.mp3';
  var stat = fs.statSync(filePath);
  res.writeHead(200, {
    'Content-Type': 'audio/mpeg',
    'Content-Length': stat.size
  });
  var readStream = fs.createReadStream(filePath);
  readStream.on('data', function(data) {
    res.write(data);
  });
  res.on('drain', function() {
    console.log('drain event fired.');
  });
  readStream.on('end', function() {
    res.end();
  });
}).listen(6969);

How to Use

When res.write returns false, the writable stream is full; the readable stream should be paused until the drain event fires, then resumed.

readStream.on('data', function(data) {
  if (!res.write(data)) {
    readStream.pause();
  }
});
res.on('drain', function() {
  readStream.resume();
});

Is This Really Necessary?

Memory‑usage tests with and without explicit back‑pressure control showed similar heap consumption, because both the readable and writable streams buffer data internally. The source of fs.createReadStream creates a buffer in memory, and Writable.write buffers data when a previous write is still in progress.

Readable.prototype.pause = function() {
  if (false !== this._readableState.flowing) {
    this._readableState.flowing = false;
    this.emit('pause');
  }
  return this;
};
if (state.writing || state.corked) {
  // buffer data
} else {
  doWrite();
}

Better Solution

The built‑in pipe method implements the same pause/resume logic automatically.

var readStream = fs.createReadStream(filePath);
readStream.pipe(res);

Internally it registers drain on the destination and pauses the source when dest.write(chunk) returns false.

dest.on('drain', ondrain);
src.on('data', ondata);
function ondata(chunk) {
  var ret = dest.write(chunk);
  if (false === ret) {
    src.pause();
  }
}

When to Use drain Directly

While pipe covers most cases, the drain event is still useful for custom back‑pressure scenarios, such as bulk inserting data into Redis while the server must remain responsive.

'use strict';
var redis = require('redis'), client = redis.createClient(),
    remaining_ops = 100000, paused = false;
function op() {
  if (remaining_ops <= 0) {
    console.error('Finished.');
    process.exit(0);
  }
  remaining_ops--;
  client.hset('test hash', 'val ' + remaining_ops, remaining_ops);
  if (client.should_buffer === true) {
    console.log('Pausing at ' + remaining_ops);
    paused = true;
  } else {
    setTimeout(op, 1);
  }
}
client.on('drain', function() {
  if (paused) {
    console.log('Resuming at ' + remaining_ops);
    paused = false;
    process.nextTick(op);
  } else {
    console.log('Got drain while not paused at ' + remaining_ops);
  }
});
op();

Conclusion

In most Node.js code you can rely on pipe for stream handling; the drain event is mainly needed when you implement manual back‑pressure. Reading the Node.js source reveals many useful details for advanced use cases.

(Unless otherwise noted, this article is licensed under CC BY‑NC‑ND 4.0)

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backend DevelopmentNode.jsbackpressuredrain event
Node Underground
Written by

Node Underground

No language is immortal—Node.js isn’t either—but thoughtful reflection is priceless. This underground community for Node.js enthusiasts was started by Taobao’s Front‑End Team (FED) to share our original insights and viewpoints from working with Node.js. Follow us. BTW, we’re hiring.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.