zlib.gunzipSync()
Decompresses gzip-formatted data synchronously and returns the original bytes as a Buffer.
zlib.gunzipSync(buffer[, options])- Added in Node.js v0.11.3
Parameters
| Parameter | Type | Description |
|---|---|---|
$buffer | Buffer | TypedArray | DataView | ArrayBuffer | string | Gzip-compressed data to expand. |
$optionsoptional | Object | zlib options: `windowBits`, `chunkSize`, `flush`, `finishFlush`, `dictionary`, `info` and `maxOutputLength`. |
Return value
BufferA Buffer holding the decompressed data. With `options.info` set to `true`, an object `{ buffer, engine }` is returned instead.
Errors
Throws synchronously when the input is not valid gzip data, when the CRC or length trailer does not match, or `ERR_BUFFER_TOO_LARGE` when the output would exceed `maxOutputLength`.
Changelog
- PHP v5.11.0The `finishFlush` option is supported, which allows truncated input to be decoded to the point it was cut off.
- PHP v14.5.0, v12.19.0The `maxOutputLength` option is supported, capping how large the decompressed result may grow.
zlib.gunzipSync() is the inverse of zlib.gzipSync(). It reads a complete gzip member — magic bytes, header, deflate stream, CRC32 and length trailer — and returns the original bytes. It verifies the checksum, so a corrupted payload raises an error rather than returning garbage.
When to reach for it
Reach for it when you already hold the entire compressed payload in memory: a .gz file read with fs.readFileSync(), a row pulled from a database, a cached HTTP body. For anything arriving over a socket or read off disk incrementally, use zlib.createGunzip() so you never buffer the whole thing.
What trips people up
- Decompression bombs. A few kilobytes can expand to gigabytes. Any input you did not compress yourself should be decompressed with
maxOutputLengthset, which turns overflow into a catchableERR_BUFFER_TOO_LARGE. - Wrong format. Feeding a raw zlib or deflate stream here fails with
Z_DATA_ERROR; those needinflateSync()orinflateRawSync(). Check for the1f 8bprefix if you are unsure what you have. - Truncated input. A partial gzip stream throws by default. Setting
finishFlush: zlib.constants.Z_SYNC_FLUSHrecovers whatever decoded successfully, at the cost of skipping the checksum.
Multiple gzip members concatenated into one buffer are decoded as a single continuous stream.
Example
When to use it
- Read a .gz log file into memory and parse it in one pass
- Expand a gzip-compressed column fetched from Postgres or MySQL
- Unpack a cached HTTP response body stored with Content-Encoding: gzip
- Verify in a test that a compression step round-trips exactly
Frequently asked questions
How do I decompress a gzip Buffer in Node.js?
Call zlib.gunzipSync(buffer), which returns the original bytes as a Buffer — add .toString() if the content was text. For data arriving in chunks, pipe through zlib.createGunzip() instead.
Why does gunzipSync throw incorrect header check?
That Z_DATA_ERROR means the input is not gzip format — most often it is a raw zlib stream, which needs inflateSync(), or a raw deflate stream, which needs inflateRawSync(). Gzip data always starts with the bytes 1f 8b.
How do I stop a gzip bomb from crashing my server?
Pass maxOutputLength with a byte budget, for example zlib.gunzipSync(input, { maxOutputLength: 1024 * 1024 }), and Node throws ERR_BUFFER_TOO_LARGE instead of allocating. Always set it for input you did not compress yourself.
More examples
Read a .gz file and count its lines
The whole archive is expanded into one string before splitting. That is fine for a small log, but a multi-gigabyte .gz would exhaust memory here — switch to createGunzip() piped into a line reader at that size.
const zlib = require('node:zlib');
const fs = require('node:fs');
// Create the archive so the example is self-contained.
const lines = Array.from({ length: 500 }, (_, i) => `GET /page/${i} 200`);
fs.writeFileSync('access.log.gz', zlib.gzipSync(lines.join('\n')));
const text = zlib.gunzipSync(fs.readFileSync('access.log.gz')).toString('utf8');
console.log('file bytes:', fs.statSync('access.log.gz').size);
console.log('lines:', text.split('\n').length);
console.log('first:', text.split('\n')[0]);
fs.unlinkSync('access.log.gz');Decompress untrusted input safely
Two guards in sequence: a magic-byte check that rejects non-gzip input before any allocation, and a size cap that converts a decompression bomb into a plain error. The second call prints the blocked message because four megabytes of zeroes exceed the 64 KB budget.
const zlib = require('node:zlib');
function safeGunzip(payload, limitBytes = 1024 * 1024) {
if (payload.length < 2 || payload[0] !== 0x1f || payload[1] !== 0x8b) {
throw new Error('not a gzip payload');
}
try {
return zlib.gunzipSync(payload, { maxOutputLength: limitBytes });
} catch (err) {
if (err.code === 'ERR_BUFFER_TOO_LARGE') {
throw new Error(`payload expands beyond ${limitBytes} bytes`);
}
throw err;
}
}
console.log(safeGunzip(zlib.gzipSync('small and safe')).toString());
try {
safeGunzip(zlib.gzipSync(Buffer.alloc(4 * 1024 * 1024)), 64 * 1024);
} catch (err) {
console.log('blocked:', err.message);
}Recover data from a truncated stream
By default a truncated gzip stream throws, because the CRC trailer is missing. Z_SYNC_FLUSH tells zlib to return everything it managed to decode and stop cleanly. The recovered text is valid but unverified, so treat it as best-effort salvage, never as trusted input.
const zlib = require('node:zlib');
const full = zlib.gzipSync('alpha beta gamma delta epsilon '.repeat(100));
const cut = full.subarray(0, full.length - 40); // simulate a broken download
try {
zlib.gunzipSync(cut);
} catch (err) {
console.log('strict mode fails:', err.code);
}
const partial = zlib.gunzipSync(cut, {
finishFlush: zlib.constants.Z_SYNC_FLUSH
});
console.log('recovered bytes:', partial.length);
console.log('tail:', JSON.stringify(partial.toString().slice(-20)));
Discussion