Skip to content
JavaScript Basics: Compress Small Buffers with zlib

JavaScript Basics: Compress Small Buffers with zlib

October 23, 2026

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 1

The tested runtime prints:

v24.19.0

1. 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.mjs

Output:

gzip round trip: true
restored: alpha,beta,gamma

gzip 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.mjs

Output:

project: guide
tags: node, compression

The 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.mjs

Output:

64-byte limit: rejected
1024-byte limit: restored 1024 bytes

The 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_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on