JavaScript Basics: Compress Small Buffers with zlib
Compression changes a byte sequence into another representation that can be restored later. Node.js supplies gzip and Brotli through node:zlib. These examples stay in memory and verify restored content instead of assuming a particular compressed size or speed.
These three independent examples were executed on Linux with Node.js v24.19.0. Use Node.js 24 for this tutorial; only the installed version above was tested. They use built-in modules, make no network requests, and assert their results. The output below is exact, not a sketch.
Prepare a scratch directory
You need Bash, Node.js on your PATH, a text editor, and the mktemp, rm, and rmdir utilities. Keep the same terminal open and save each named file as UTF-8 in the new directory. The .mjs extension selects an ES module.
node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1The tested runtime prints:
v24.19.01. Round-trip UTF-8 text through gzip
Save this as roundtrip-gzip-text.mjs:
import assert from 'node:assert/strict';
import { gzip, gunzip } from 'node:zlib';
import { promisify } from 'node:util';
const compress = promisify(gzip);
const decompress = promisify(gunzip);
const original = Buffer.from('alpha,beta,gamma', 'utf8');
const packed = await compress(original);
const restored = await decompress(packed);
assert.ok(Buffer.isBuffer(packed));
assert.deepEqual(restored, original);
console.log(`gzip round trip: ${restored.equals(original)}`);
console.log(`restored: ${restored.toString('utf8')}`);Run it:
node roundtrip-gzip-text.mjsOutput:
gzip round trip: true
restored: alpha,beta,gammagzip and gunzip form a matching pair. promisify adapts their callbacks so the script can await each operation. Compressed output is binary: decode the restored bytes as UTF-8, not the compressed bytes. Small inputs can become larger because the format has overhead, so successful compression does not promise a smaller result. See the zlib convenience methods.
2. Choose a Brotli quality setting explicitly
Save this as roundtrip-brotli-json.mjs:
import assert from 'node:assert/strict';
import { brotliCompress, brotliDecompress, constants } from 'node:zlib';
import { promisify } from 'node:util';
const compress = promisify(brotliCompress);
const decompress = promisify(brotliDecompress);
const record = { project: 'guide', tags: ['node', 'compression'] };
const source = Buffer.from(JSON.stringify(record), 'utf8');
const packed = await compress(source, {
params: { [constants.BROTLI_PARAM_QUALITY]: 4 }
});
const restored = JSON.parse((await decompress(packed)).toString('utf8'));
assert.deepEqual(restored, record);
console.log(`project: ${restored.project}`);
console.log(`tags: ${restored.tags.join(', ')}`);Run it:
node roundtrip-brotli-json.mjsOutput:
project: guide
tags: node, compressionThe object is serialized to JSON before compression and parsed after decompression. Brotli quality is configured through a numeric constant used as a computed property name, not a top-level quality field. The value 4 is an example choice, not a universal optimum; compare CPU cost and size with representative data before tuning. Keep track of the format so the reader selects Brotli rather than gunzip. See Brotli options.
3. Reject decompressed output above a byte limit
Save this as limit-gzip-output.mjs:
import assert from 'node:assert/strict';
import { gzip, gunzip } from 'node:zlib';
import { promisify } from 'node:util';
const compress = promisify(gzip);
const decompress = promisify(gunzip);
const packed = await compress(Buffer.alloc(1024, 'a'));
await assert.rejects(
decompress(packed, { maxOutputLength: 64 }),
{ code: 'ERR_BUFFER_TOO_LARGE' }
);
const restored = await decompress(packed, { maxOutputLength: 1024 });
assert.equal(restored.length, 1024);
assert.ok(restored.every(byte => byte === 97));
console.log('64-byte limit: rejected');
console.log(`1024-byte limit: restored ${restored.length} bytes`);Run it:
node limit-gzip-output.mjsOutput:
64-byte limit: rejected
1024-byte limit: restored 1024 bytesThe same compressed input fails with a smaller output budget and succeeds when the budget covers its restored bytes. maxOutputLength applies to the convenience methods and counts bytes. It is not a general streaming limit, a compressed-input limit, or a CPU-time budget. Validate input sizes and control concurrency too when processing untrusted data. The second attempt is appropriate here only because this test created the trusted input itself. See zlib options.
Practical limits
The asynchronous APIs avoid running the compression operation synchronously on the JavaScript thread, but they still consume CPU and use the shared threadpool. Avoid launching an unbounded number at once. These buffer-based examples hold whole inputs and outputs in memory; for larger data, use zlib streams with a pipeline and an explicit output budget where needed. Compression provides neither encryption nor authenticity.
Clean up
In the same terminal, remove only the three named source files, return to your starting directory, and remove the now-empty scratch directory:
rm -- roundtrip-gzip-text.mjs roundtrip-brotli-json.mjs limit-gzip-output.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.