zlib.createGunzip()
Creates a Transform stream that decompresses gzip data as it flows through.
zlib.createGunzip([options])- Added in Node.js v0.5.8
Parameters
| Parameter | Type | Description |
|---|---|---|
$optionsoptional | Object | zlib options: `windowBits`, `chunkSize`, `flush`, `finishFlush`, `dictionary` and `maxOutputLength`, plus any Transform stream option. |
Return value
zlib.GunzipA new `zlib.Gunzip` object — a Transform stream whose readable side emits the decompressed data.
Errors
Emits an `'error'` event — not a throw — for invalid gzip data, a truncated stream, or trailing garbage after the last member.
Changelog
- PHP v5.0.0A truncated input stream now results in an `'error'` event instead of silently ending.
- PHP v5.9.0Multiple concatenated gzip file members are supported.
- PHP v6.0.0Trailing garbage at the end of the input stream now results in an `'error'` event.
- PHP v14.5.0, v12.19.0The `maxOutputLength` option is supported.
zlib.createGunzip() is the streaming inverse of zlib.createGzip(). Write gzip bytes in, read plain bytes out, with memory proportional to the chunk size rather than to the archive. It handles the case that makes streaming essential: a 200 MB .gz that expands to 4 GB can be processed line by line without ever holding the whole thing.
When to reach for it
Any time the compressed data is large or arrives incrementally — reading a compressed log, consuming a gzip HTTP response body, restoring a database dump. For a small buffer already in memory, gunzipSync() is simpler.
What trips people up
- Errors are events, not exceptions. A
try/catcharound the pipe does nothing. Corrupt input emits'error', and with no listener that becomes an uncaught exception which takes the process down.stream.pipeline()handles this for you. - Strictness. Since v5.0.0 a truncated stream errors, and since v6.0.0 so does any trailing garbage after the last gzip member. Legitimately concatenated gzip members, however, are decoded as one continuous stream — that is how
cat a.gz b.gzworks. - Bombs. Streaming does not make a decompression bomb safe. Set
maxOutputLength, or count bytes as they arrive and destroy the stream past a budget.
Example
When to use it
- Read a huge compressed log line by line without expanding it to disk
- Decompress a gzip HTTP response body as it downloads
- Restore a .sql.gz database dump straight into a client
- Round-trip data through compression in a test without touching the filesystem
Frequently asked questions
How do I read a large gzip file in Node.js without running out of memory?
Pipe fs.createReadStream('file.gz') through zlib.createGunzip() and consume the output incrementally, for example with readline, so memory stays proportional to the chunk size rather than the expanded content.
Why does my gunzip stream crash the process instead of throwing?
Stream failures arrive as 'error' events, so a try/catch around the pipe never fires and an unhandled 'error' becomes an uncaught exception. Use stream.pipeline, which converts the event into a rejected promise or a callback error.
Can createGunzip read several gzip files concatenated together?
Yes — since Node 5.9.0 concatenated gzip members decode as one continuous stream, which is what makes cat a.gz b.gz valid. Non-gzip trailing bytes after the last member still raise an error.
More examples
Read a huge .gz log line by line
Memory stays constant whether the archive holds a thousand lines or a billion, because readline consumes the decompressed stream one line at a time. Both streams get their own error listener — without them, a corrupt archive raises an uncaught exception rather than the message printed here.
import zlib from 'node:zlib';
import fs from 'node:fs';
import readline from 'node:readline';
const gunzip = zlib.createGunzip();
const source = fs.createReadStream('access.log.gz');
source.on('error', (err) => console.error('read failed:', err.message));
gunzip.on('error', (err) => console.error('bad archive:', err.code));
const rl = readline.createInterface({
input: source.pipe(gunzip),
crlfDelay: Infinity
});
let total = 0;
let errors = 0;
for await (const line of rl) {
total++;
if (line.includes(' 500 ')) errors++;
}
console.log(`${total} lines scanned, ${errors} server errors`);Guard a stream against a decompression bomb
The counter runs inside the pipeline, so throwing tears down the gunzip stream immediately rather than after the full expansion. Twenty megabytes of zeroes compress to about twenty kilobytes; without this cap the stream would happily produce all twenty megabytes.
import zlib from 'node:zlib';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
async function gunzipCapped(compressed, limitBytes) {
let seen = 0;
const gunzip = zlib.createGunzip();
await pipeline(Readable.from([compressed]), gunzip, async function* (stream) {
for await (const chunk of stream) {
seen += chunk.length;
if (seen > limitBytes) throw new Error(`exceeded ${limitBytes} bytes`);
yield chunk;
}
});
return seen;
}
const bomb = zlib.gzipSync(Buffer.alloc(20 * 1024 * 1024, 0));
console.log('compressed:', bomb.length, 'bytes');
try {
await gunzipCapped(bomb, 256 * 1024);
} catch (err) {
console.log('aborted:', err.message);
}Concatenated members decode as one stream
Three separate gzip members concatenated byte-for-byte decode into one continuous output, which is why appending to a .gz file works and why rotated logs can be joined. Non-gzip bytes after the final member are a different matter: since Node 6 they raise an error instead of being silently dropped.
import zlib from 'node:zlib';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
// Equivalent to `cat part1.gz part2.gz part3.gz > all.gz`
const joined = Buffer.concat([
zlib.gzipSync('first member\n'),
zlib.gzipSync('second member\n'),
zlib.gzipSync('third member\n')
]);
const out = [];
await pipeline(
Readable.from([joined]),
zlib.createGunzip(),
async function* (s) { for await (const c of s) out.push(c); }
);
console.log(JSON.stringify(Buffer.concat(out).toString()));
// Trailing bytes after the last member are an error, not ignored.
try {
await pipeline(
Readable.from([Buffer.concat([joined, Buffer.from('junk')])]),
zlib.createGunzip(),
async function* (s) { for await (const c of s) void c; }
);
} catch (err) {
console.log('trailing garbage:', err.code);
}
Discussion