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.
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)
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
