fs.createWriteStream()
Opens a file as a writable stream for incremental writes without holding the whole output in memory.
fs.createWriteStream(path[, options])- Added in Node.js v0.1.31
Parameters
| Parameter | Type | Description |
|---|---|---|
$path | string | Buffer | URL | The file to write. Ignored when options.fd is supplied. |
$optionsoptional | string | Object | Stream options. If a string is passed it is taken as the encoding. |
$options.flagsoptional | string= 'w' | File system flags. 'w' truncates, 'a' appends, 'r+' opens for update without truncating, 'wx' fails if the file exists. |
$options.encodingoptional | string= 'utf8' | Encoding used for string writes. |
$options.fdoptional | integer | FileHandle= null | An already-open descriptor to write to. When given, path is ignored and no 'open' event is emitted. |
$options.modeoptional | integer= 0o666 | File mode, applied only if the file is created. |
$options.autoCloseoptional | boolean= true | Close the descriptor automatically on 'error' or 'finish'. When false you must close it yourself. |
$options.emitCloseoptional | boolean= true | Emit a 'close' event after the stream is destroyed. |
$options.startoptional | integer | Byte offset at which to begin writing. Usually paired with flags: 'r+' to patch a file in place. |
$options.fsoptional | Object | null= null | Overrides for open, write, writev and close. At least one of write or writev is required when this is used. |
$options.signaloptional | AbortSignal | null= null | Aborting the corresponding controller destroys the stream. |
$options.highWaterMarkoptional | number | Buffer threshold above which write() returns false. Defaults to stream.getDefaultHighWaterMark(). |
$options.flushoptional | boolean= false | Flush the underlying descriptor (fsync) before closing it. |
$options.windowsHandleoptional | bigint= null | A raw Win32 HANDLE to write to in place of fd. Windows only; throws on other platforms. |
Return value
fs.WriteStreamAn fs.WriteStream, a standard Writable that emits 'open', 'ready', 'drain', 'finish', 'close' and 'error' events and exposes a bytesWritten property.
Errors
A path that cannot be opened does not throw synchronously — it surfaces as an 'error' event carrying EACCES, ENOENT or EEXIST (with flags: 'wx'), and writing after end() emits an ERR_STREAM_WRITE_AFTER_END error rather than throwing at the call site.
Changelog
- PHP v26.8.0Added the windowsHandle option.
- PHP v22.0.0The default highWaterMark was raised, changing how often write() returns false and how large each disk write is.
- PHP v21.0.0, v20.10.0Added the flush option, which fsyncs the descriptor before closing so data is durable rather than merely handed to the OS.
- PHP v15.5.0Added support for AbortSignal via the signal option.
- PHP v14.0.0The emitClose default changed to true.
- PHP v7.6.0The path parameter can be a WHATWG URL object using the file: protocol.
- PHP v5.5.0The autoClose option is supported.
fs.createWriteStream() returns an fs.WriteStream, a writable stream over a file descriptor. It suits output produced over time — an append-only log, a CSV assembled row by row, a response body being saved — where buffering everything for a single fs.writeFile() would be wasteful or impossible.
The default flags is 'w', which truncates the file the moment the stream opens, even if you never write anything. Use 'a' to append, 'wx' to fail when the file already exists, and 'r+' with start to patch bytes in place.
Ordering and backpressure
write() returns false once the internal buffer exceeds highWaterMark. Ignoring that return value still produces correct, in-order output — but the queue grows in memory without bound, which is how a fast producer becomes an out-of-memory crash. Wait for the 'drain' event before writing more, or hand the stream to stream.pipeline() and let it handle backpressure for you.
Knowing when it is done
Call end(), then wait for 'finish' before reading the file back or reporting success. Reading straight after end() races the flush and frequently sees a short or empty file — one of the most common bug reports against this API.
Example
When to use it
- Append structured entries to an application log without reopening the file each time.
- Stream a generated CSV or JSON export straight to disk as rows are produced.
- Save an incoming upload or HTTP response body to a file via stream.pipeline().
- Patch a fixed-width record in place with flags: 'r+' and a start offset.
Frequently asked questions
How do I know when a write stream has finished writing to the file?
Call end() and then wait for the 'finish' event — or use stream.pipeline(), whose promise resolves at the same point. Reading the file immediately after end() returns races the flush and often shows partial or empty content, since end() only queues the final write.
Why is my file empty or truncated after using createWriteStream?
Almost always because the file was read before 'finish' fired, or because the stream was opened with the default flags: 'w', which truncates the file the instant it opens. Await 'finish' before reading, and use flags: 'a' when you meant to append.
What does it mean when write() returns false?
It means the stream's internal buffer has passed highWaterMark and you should stop writing until the 'drain' event fires. The write is still accepted and ordering is preserved, but continuing to ignore the signal lets the buffer grow unboundedly in memory — which is how a fast loop writing to a slow disk turns into an out-of-memory crash.
More examples
Append instead of truncate
The output is [ 'first', 'second' ] — both lines survive. Swap flags to 'w' and you get [ 'second' ] only, because the stream truncates the file at open time rather than at first write. That timing detail matters: a stream opened with 'w' and then abandoned without a single write still leaves an empty file behind.
import fs from 'node:fs';
import fsp from 'node:fs/promises';
import { once } from 'node:events';
// Each pass reopens the file. With the default flags: 'w' the second
// pass would wipe the first line; 'a' appends to whatever is there.
for (const msg of ['first', 'second']) {
const s = fs.createWriteStream('audit.log', { flags: 'a' });
s.end(msg + '\n');
await once(s, 'finish');
}
console.log((await fsp.readFile('audit.log', 'utf8')).trimEnd().split('\n'));
await fsp.unlink('audit.log');Copy a file with pipeline, which handles backpressure for you
It prints 1024, and the await returns only after the write side has finished — so the stat that follows is safe. This is the version to reach for in real code: with a bare .pipe() an error on the read side leaves the write stream open and the destination file truncated, whereas pipeline rejects and cleans up both ends.
import fs from 'node:fs';
import fsp from 'node:fs/promises';
import { pipeline } from 'node:stream/promises';
await fsp.writeFile('source.bin', Buffer.alloc(1024, 7));
// pipeline waits for 'finish', propagates errors from either side,
// and destroys both streams so no descriptor leaks.
await pipeline(
fs.createReadStream('source.bin'),
fs.createWriteStream('copy.bin'),
);
console.log('copied bytes:', (await fsp.stat('copy.bin')).size);
await fsp.unlink('source.bin');
await fsp.unlink('copy.bin');
Discussion