fs.createReadStream()
Opens a file as a readable stream so large files are processed in chunks instead of loaded at once.
fs.createReadStream(path[, options])- Added in Node.js v0.1.31
Parameters
| Parameter | Type | Description |
|---|---|---|
$path | string | Buffer | URL | The file to read. 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= 'r' | File system flags used when opening the file. |
$options.encodingoptional | string= null | Any encoding accepted by Buffer. When set, chunks are strings; when null, chunks are Buffers. |
$options.fdoptional | integer | FileHandle= null | An already-open descriptor to read from. 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 'end'. When false you must close it yourself. |
$options.emitCloseoptional | boolean= true | Emit a 'close' event after the stream is destroyed. |
$options.startoptional | integer | First byte offset to read, inclusive, counting from 0. |
$options.endoptional | integer= Infinity | Last byte offset to read, inclusive. |
$options.highWaterMarkoptional | integer= 64 * 1024 | Maximum bytes buffered before the stream stops reading ahead. |
$options.fsoptional | Object | null= null | Overrides for the open, read and close implementations. An override for read is required when this is used. |
$options.signaloptional | AbortSignal | null= null | Aborting the corresponding controller destroys the stream. |
$options.windowsHandleoptional | bigint= null | A raw Win32 HANDLE to read from in place of fd. Windows only; throws on other platforms. |
Return value
fs.ReadStreamAn fs.ReadStream, a standard Readable that emits 'open', 'ready', 'data', 'end', 'close' and 'error' events.
Errors
Invalid start or end values throw ERR_OUT_OF_RANGE synchronously, but a missing or unreadable file does not throw — it surfaces asynchronously as an 'error' event carrying ENOENT or EACCES, which becomes an uncaught exception if no 'error' listener is attached.
Changelog
- PHP v26.8.0Added the windowsHandle option.
- PHP v15.5.0Added support for AbortSignal via the signal option.
- PHP v15.4.0The fd option accepts FileHandle arguments.
- PHP v14.0.0The emitClose default changed to true, so streams now emit 'close' after being destroyed unless you opt out.
- PHP v11.0.0New restrictions on start and end; out-of-range values now throw ERR_OUT_OF_RANGE instead of behaving unpredictably.
- PHP v7.6.0The path parameter can be a WHATWG URL object using the file: protocol.
- PHP v2.3.0The options argument may be a string specifying the encoding.
fs.createReadStream() returns an fs.ReadStream — an ordinary readable stream backed by a file descriptor. Instead of materialising the whole file as one Buffer the way fs.readFile() does, it pulls highWaterMark bytes at a time (64 KiB by default), so peak memory stays flat whether the file is 5 KB or 5 GB.
Reach for it when the file is large or unbounded, when you want to start work before the last byte arrives, or when the data is heading somewhere that already speaks streams: an HTTP response, a gzip transform, readline for line-by-line parsing, or another file through stream.pipeline().
Things that bite
- The file is opened after the call returns. A missing path does not throw from
createReadStream(); it arrives as an'error'event, and with no listener that crashes the process. - Without an
encodingyou getBufferchunks. With one you get strings, and multibyte characters are decoded safely across chunk boundaries. startandendare byte offsets and both are inclusive, so{ start: 0, end: 9 }reads ten bytes, not nine.- Chunk boundaries are arbitrary. Never assume a chunk is a whole line or a complete JSON document.
- Prefer
stream.pipeline()over.pipe()so errors propagate and descriptors are always closed.
Example
When to use it
- Serve a large video or export file over HTTP without buffering it in memory.
- Parse a multi-gigabyte CSV or NDJSON log line by line with readline.
- Read only the last few kilobytes of a log file using start and end offsets.
- Pipe a file through zlib.createGzip() into a compressed copy on disk.
Frequently asked questions
When should I use fs.createReadStream instead of fs.readFile?
Use createReadStream when the file is large enough that holding it entirely in memory is a problem, or when you can start processing before the file ends — parsing logs, serving downloads, piping through compression. Use readFile for small configuration and template files, where a single Buffer is simpler and the whole content is needed at once anyway.
Why doesn't fs.createReadStream throw when the file doesn't exist?
Because the open happens asynchronously after the call returns, the ENOENT is delivered as an 'error' event on the returned stream rather than as a synchronous throw. Always attach an 'error' listener, or use stream.pipeline() which propagates the error to its callback or promise.
How do I read a file line by line in Node.js?
Wrap the stream with readline.createInterface({ input: fs.createReadStream(path), crlfDelay: Infinity }) and iterate it with for await. Splitting raw 'data' chunks on newlines is unreliable because chunk boundaries fall at arbitrary byte offsets and routinely cut a line in half.
More examples
Read a specific byte range
The output is '6789' with length 4, not 3 — end: 9 includes byte 9. This is the mechanism behind HTTP range requests and tail-style log readers: only the requested bytes are read from disk, so grabbing the last kilobyte of a 10 GB file costs one seek. Note the stream is consumed with for await, which is the async-iterator interface every readable stream supports.
import fs from 'node:fs';
import fsp from 'node:fs/promises';
await fsp.writeFile('numbers.txt', '0123456789');
// start and end are both INCLUSIVE byte offsets.
const tail = fs.createReadStream('numbers.txt', { start: 6, end: 9, encoding: 'utf8' });
let out = '';
for await (const chunk of tail) out += chunk;
console.log('bytes 6..9:', out);
console.log('length:', out.length);
await fsp.unlink('numbers.txt');Count lines without buffering the file
It prints 500. readline sits on top of the stream and does the reassembly that raw 'data' chunks cannot — a line split across two chunks is joined correctly, which is exactly the bug you get from splitting each chunk on '\n' yourself. Memory use is bounded by highWaterMark plus one line, so the same code works unchanged on a file far larger than RAM.
import fs from 'node:fs';
import fsp from 'node:fs/promises';
import readline from 'node:readline';
await fsp.writeFile('big.txt', Array.from({ length: 500 }, (_, i) => `row ${i}`).join('\n') + '\n');
const rl = readline.createInterface({
input: fs.createReadStream('big.txt', { encoding: 'utf8' }),
crlfDelay: Infinity, // treat \r\n as one break
});
let lines = 0;
for await (const line of rl) lines++;
console.log('lines:', lines);
await fsp.unlink('big.txt');
Discussion