pause-stream
pause-stream is a classic Node stream that buffers everything written to it while paused and flushes the buffer when you resume. You attach it between a producer you cannot control and a consumer that is not ready yet, call pause(), and no data is lost in between. The important thing to know before you install it: version 0.0.11 has exactly two lines of code. Its index.js reads 'through@2 handles this by default!' followed by module.exports = require('through'). require('pause-stream') and require('through') return the identical function object, verified by strict equality. Everything the package does is done by through 2.3.8, which the package depends on.
pause-stream is a two-line re-export of through that has been archived since 2018 and unreleased since 2013, and 6.1 million weekly downloads do not change that. If you must keep classic streams, depend on through directly; if you are writing anything new, PassThrough from node:stream is the honest answer.
Use it if
- You are maintaining a codebase from the streams1 era that already imports pause-stream and you want to understand what it actually does before touching it
- A producer starts emitting before your consumer is wired up and you need a small buffer in between, on old-style Node streams that predate Readable and Writable
- You need pause and resume semantics where write() returns false while paused and buffered chunks replay in order on resume, with no object-mode or highWaterMark configuration
- Something deep in your dependency tree already pulls it in and you are deciding whether to dedupe it against through rather than remove it
- You are writing new code: this is streams1 built on the bare Stream class, not Readable or Writable, so it has no _read, no highWaterMark, and no working destroy path for stream.pipeline
- You want error propagation: classic .pipe() does not forward errors, and pipeline() cannot clean this stream up properly, so a failure downstream leaves the buffer and the source alive
- Memory matters under load: the buffer is a plain array with no size limit, so a paused stream in front of a fast producer grows until the process runs out of heap
- You could just depend on through instead, which is the same code with 46 million weekly downloads against this wrapper's 6.1 million, one less package, and a README that is actually about the code you are running
- You need a maintained package: the repo was archived in June 2018, the last publish was version 0.0.11 in September 2013, and the npm maintainer account is a placeholder rather than a named person
- You are on modern Node and want PassThrough, which is in core, honors backpressure, works with pipeline(), and needs no dependency at all
Setup reality
Installation is npm install pause-stream and nothing else: one dependency on through ~2.3, no native build, no config. The real setup surprise is that there is nothing to set up because there is nothing here. Reading index.js takes five seconds and settles the question: the module body is a comment and a re-export of through. So every question you have about pause-stream is really a question about through 2.3.8, and the pause-stream README is a 2013 document describing behavior that lives in a different package. Practical consequences follow from that. The stream you get back is constructed with new Stream(), the legacy base class, so it is not an instanceof Readable and modern helpers treat it as a foreign object. pause() sets a flag and returns the stream, which is why the README's badlyBehavedStream.pipe(ps.pause()) chains. While paused, write() returns false and chunks accumulate in an in-memory array; resume() drains that array by emitting data events synchronously, then emits drain. Nothing bounds the array, so pausing in front of a producer that ignores the false return value is an unbounded memory buffer with a friendly name. There is a destroy() method, but it only sets flags, empties the buffer and emits close, which is not what stream.pipeline expects, so mixing this stream into a pipeline() call will not give you the cleanup you think it does. If you are adding a buffer to new code, PassThrough from node:stream does the same job with real backpressure and no dependency.
Patterns
Check what you actually installedconfirm-it-is-through
const pauseStream = require('pause-stream')
const through = require('through')
console.log(pauseStream === through)
// true
// package/index.js in pause-stream@0.0.11 is:
// //through@2 handles this by default!
// module.exports = require('through')Strict equality holds because the module re-exports the same function object. Any behaviour question about pause-stream is a question about through 2.3.8.
Hold data until the destination existsbuffer-until-consumer-ready
const pause = require('pause-stream')
const ps = pause()
source.pipe(ps.pause())
lookupDestination((err, dest) => {
if (err) return ps.destroy()
ps.pipe(dest)
ps.resume()
})pause() returns the stream so it chains inside pipe(). Chunks that arrive before resume() sit in a plain array with no upper bound.
See the buffering happenpause-and-resume
const pause = require('pause-stream')
const ps = pause()
const seen = []
ps.on('data', (chunk) => seen.push(String(chunk)))
ps.pause()
ps.write('a')
ps.write('b')
console.log(seen.length) // 0
ps.resume()
console.log(seen.join(',')) // 'a,b'resume() drains synchronously by emitting data events in order, then emits drain. Order is preserved; timing is not.
Stop writing when write() says falserespect-write-return
const pause = require('pause-stream')
const ps = pause()
ps.pause()
if (ps.write(chunk) === false) {
producer.pause()
ps.once('drain', () => producer.resume())
}write() returns !paused, so it is the only backpressure signal there is. Ignoring it is how the internal array grows without limit.
Pass write and end handlers like through doestransform-with-handlers
const pause = require('pause-stream')
const upper = pause(
function (data) { this.queue(String(data).toUpperCase()) },
function () { this.queue(null) }
)
source.pipe(upper).pipe(destination)This is through's constructor signature, undocumented in the pause-stream README. queue(null) is how you signal end from inside a handler.
Attach error listeners on every hophandle-errors-manually
const ps = require('pause-stream')()
source.on('error', onError)
ps.on('error', onError)
destination.on('error', onError)
source.pipe(ps).pipe(destination)
function onError (err) {
source.destroy && source.destroy()
ps.destroy()
}Classic pipe() does not forward errors between streams. Every stream in the chain needs its own listener or an error becomes an uncaught exception.
Do not put it inside stream.pipelineavoid-pipeline
// Does not clean up the way you expect:
// await pipeline(source, require('pause-stream')(), destination)
// Use the modern equivalent instead:
const { PassThrough, pipeline } = require('node:stream')
const { promisify } = require('node:util')
await promisify(pipeline)(source, new PassThrough(), destination)destroy() here only flips flags and emits close, so pipeline() cannot tear the chain down. PassThrough implements the contract pipeline expects.
Swap in a core PassThroughreplace-with-passthrough
const { PassThrough } = require('node:stream')
const buffer = new PassThrough({ highWaterMark: 1 << 20 })
source.pipe(buffer)
buffer.pause()
later(() => {
buffer.pipe(destination)
buffer.resume()
})highWaterMark gives the bound that pause-stream never had, and the source is told to slow down instead of the buffer growing.
Collapse the duplicate dependencydedupe-against-through
// package.json
// - "pause-stream": "0.0.11",
// + "through": "~2.3"
-const pause = require('pause-stream')
+const through = require('through')
-const ps = pause()
+const ps = through()Behaviour is identical because it is the same object. You lose one package from the tree and gain a README that describes the running code.
Watch the unbounded buffer while pausedinspect-buffer-growth
const ps = require('pause-stream')()
ps.pause()
let written = 0
const timer = setInterval(() => {
console.log({ written, heapMb: Math.round(process.memoryUsage().heapUsed / 1e6) })
}, 1000)
for (const chunk of firehose) {
ps.write(chunk)
written++
}
clearInterval(timer)There is no highWaterMark and no limit check in through's queue(), so heap use tracks how much you wrote while paused.
Push objects through without configurationobject-mode-note
const ps = require('pause-stream')()
ps.on('data', (record) => console.log(record.id))
ps.pause()
ps.write({ id: 1 })
ps.write({ id: 2 })
ps.resume()There is no objectMode flag because there is no mode: chunks are stored and re-emitted as-is. Nothing converts to Buffer and nothing validates the type.
Know what end and destroy actually doend-and-destroy
const ps = require('pause-stream')()
ps.end('last chunk') // writes, then ends; repeat calls are ignored
ps.destroy() // clears the buffer, flips flags, emits 'close'
console.log(ps.writable, ps.readable) // false falsedestroy() emits close but never error, and it does not touch the upstream source. Whatever is feeding this stream keeps running until you stop it yourself.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| through | npm | You want the package that pause-stream re-exports, without the extra hop, and you are keeping classic stream code alive |
| through2 | npm | You want the same shape of tiny transform helper but built on streams2 so pipe, backpressure and errors behave like the rest of modern Node |
| readable-stream | npm | You need the userland copy of Node's current stream implementation and can build the buffer with PassThrough yourself |