Node.js Compression Formats and HTTP

Compare gzip, zlib deflate, and Brotli in Node.js, bound decompression output, and understand HTTP encoding negotiation and cache headers.

Lesson 32 of 43Intermediate10 min read

Compression trades CPU work and buffering for a potentially smaller byte representation. That trade is helpful for repetitive JSON or text, but a JPEG or an already compressed archive may barely shrink. Node's zlib module exposes several formats, and choosing the wrong decompressor is a format error rather than a text-encoding problem. This Node 24 reference extends the basic stream compression lesson with complete gzip and Brotli round trips, resource limits, and HTTP delivery rules. Its examples compress in-memory fixtures so every stated result can be checked without creating an archive or contacting another service.

Distinguish formats from encodings

UTF-8 represents text as bytes; compression transforms those bytes into another representation. Decode text after decompression, not before. gzip adds a defined wrapper and integrity fields around compressed data. zlib-wrapped deflate and raw deflate have different framing despite sharing a compression family. Brotli uses another format. A file extension or a content-type alone does not instruct the runtime which decompressor to use. Choose matching APIs such as gzip with gunzip, or brotliCompress with brotliDecompress.

Keep the format in an explicit contract when storing data. If a record says its payload is Brotli, validate that field and use the corresponding decoder. Do not try every decompressor on arbitrary input as a substitute for a format rule. Raw deflate is appropriate only where the protocol expressly requires that wrapperless representation. Corruption can cause decoding errors even with the correct format, and successful decompression does not establish that the resulting JSON or application data is valid.

Choose asynchronous work and sensible quality

The synchronous zlib methods finish before returning and block the event loop while doing their work. They can be reasonable for a bounded one-off startup task, but server handlers should usually use asynchronous callbacks, promisified methods, or streams. Asynchronous compression still consumes real CPU resources. Launching thousands of compression jobs at once can saturate the worker pool and delay other work. Limit concurrency and measure a realistic workload.

Brotli and gzip provide settings that influence size, speed, and memory use. The highest compression setting is not automatically the best production choice. A static asset built once can tolerate more expensive compression than a short API response generated on every request. Compare representative payloads and include latency and CPU consumption in the decision. Tiny responses can grow after format overhead, and already compressed content usually wastes CPU. The fixture checks byte equality after a round trip rather than claiming a universal compression ratio from one repetitive sentence.

Send the correct HTTP representation headers

A compressed HTTP JSON response still has a content type of application/json. Content-Encoding declares the transformation applied to the representation, such as gzip or br. Content-Length, if supplied, describes the transmitted compressed bytes. Setting it from the original text length gives the receiver an incorrect body boundary. For a streamed response, let the server use its supported transfer behavior instead of guessing the final compressed size.

Accept-Encoding expresses what the client accepts and can contain quality values, including zero to refuse a format. A proper negotiator must respect those values and the identity representation rules. A substring search for br can produce incorrect decisions. When the response varies by Accept-Encoding, emit Vary: Accept-Encoding so shared caches distinguish representations. Do not compress a body twice because both application code and a proxy apply compression. HTTP clients such as fetch can decode supported response encodings automatically, so inspect their documented behavior before manually decoding a returned body.

Bound decompression as well as compressed input

A small compressed body can expand into a very large result. Limiting only the incoming compressed byte count does not bound the memory needed after decompression. For the one-shot methods used here, maxOutputLength supplies a limit on the resulting allocation. A streaming pipeline needs its own accounting and a termination policy when expanded bytes exceed the application limit. Limits should reflect the expected format and product behavior rather than the largest value the machine might theoretically allocate.

Treat an expansion-limit failure as invalid or unacceptable input, not as an invitation to retry with no limit. Also bound request duration and concurrent decompression jobs. Format checksums detect some corruption but are not an authentication mechanism; an attacker can create a valid compressed payload with harmful size or content. After a bounded successful decode, validate the application structure. Compression of sensitive values alongside attacker-controlled text can also create information leaks in some protocols, so enable dynamic compression according to the application's actual data and threat model.

Node.js documentation and version notes

Node.js Compression Formats and HTTP Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Round-tripping text through gzip and Brotli

This complete .mjs fixture uses promisify to await both formats. The source is a repetitive UTF-8 message, so both encoded buffers are smaller than the original. Each decoder receives the matching format and a limit larger than the known source. Buffer.equals checks exact byte restoration before the text is interpreted. The output avoids a hard-coded compressed byte length, since algorithm settings and library versions can change the precise representation while preserving correctness. The size comparison is a claim about this controlled payload, not every JSON document, image, or archive a server might encounter.

JavaScript
import { gzip, gunzip, brotliCompress, brotliDecompress } from 'node:zlib';
import { promisify } from 'node:util';
const source = Buffer.from('repeated catalog description; '.repeat(100));
for (const [name, encode, decode] of [
  ['gzip', promisify(gzip), promisify(gunzip)],
  ['br', promisify(brotliCompress), promisify(brotliDecompress)]
]) {
  const compressed = await encode(source);
  const restored = await decode(compressed, { maxOutputLength: 4096 });
  console.log(name, restored.equals(source), compressed.length < source.length);
}
Output
gzip true true
br true true

Node.js Compression Formats and HTTP in Real Projects

Enforce an output limit when decoding gzip

Decode the same valid gzip body under two different output limits. The first limit is large enough for its one hundred decoded bytes; the second allows only ten. Print a stable rejection label for the second case rather than relying on an entire runtime error message. This practice demonstrates an expansion limit with a small harmless fixture. It does not require constructing a dangerous archive. Keep the compressed input constant so the observed difference comes from the receiver's resource policy. A failed bounded decode must not be followed by an unlimited decode in a real request path.

JavaScript
import { gzip, gunzip } from 'node:zlib';
import { promisify } from 'node:util';
const pack = promisify(gzip);
const unpack = promisify(gunzip);
const compressed = await pack(Buffer.from('x'.repeat(100)));
console.log((await unpack(compressed, { maxOutputLength: 100 })).length);
try { await unpack(compressed, { maxOutputLength: 10 }); }
catch (error) {
  if (error.code !== 'ERR_BUFFER_TOO_LARGE') throw error;
  console.log('Expansion limit rejected');
}
Output
100
Expansion limit rejected

Node.js Compression Formats and HTTP FAQ

Is gzip interchangeable with raw deflate?

No. Their surrounding byte formats differ. A decoder expecting one representation can reject another even when both involve the deflate compression algorithm. Select the format required by the storage or network contract and pair the corresponding producer and consumer APIs. Guessing from a filename is insufficient. The fixture explicitly selects matching functions, which is clearer than hiding format discovery inside a generic decode helper.

Why does a JSON response keep its JSON content type?

Content-Type describes the underlying media representation, while Content-Encoding describes compression applied for transmission. A gzip-compressed JSON response can therefore declare application/json and gzip together. If Content-Length is present, count the compressed transmitted bytes. Keep cache variation correct when clients receive different encodings. Changing the media type to an archive type would communicate a different resource contract to clients that expect JSON.

Can asynchronous compression overload a server?

Yes. It avoids blocking JavaScript while work runs, but it still needs CPU and worker capacity. A burst of expensive jobs can delay other operations sharing resources. Bound concurrency, avoid recompressing already compressed content, and cache suitable static results. Measure the size benefit and time cost for representative payloads. An asynchronous API is a scheduling choice, not unlimited computational capacity.

Does a compressed input limit prevent large output?

No. Expansion can be much larger than the input. Enforce an output bound as well, using a supported one-shot limit or explicit streaming byte accounting. Reject input that exceeds the chosen policy and close or cancel its processing. Then validate the decoded document. A format checksum can detect corruption, but it does not authenticate a sender or make a large correctly encoded payload acceptable.

Node.js Compression Formats and HTTP Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

What happens to the valid 100-byte gzip fixture under maxOutputLength: 10?

Node.js Compression Formats and HTTP Quiz

2 questions to check what you learned.

  1. Which HTTP length describes a compressed response body?

    • The number of transmitted compressed bytes
    • The original JavaScript string length
    • The number of JSON object properties
  2. Why emit Vary: Accept-Encoding when encodings differ?

    • Caches need to distinguish representations selected by that request header
    • It forces every client to accept Brotli
    • It disables content type validation