Brotli wins on size for text; Gzip wins on encode latency at high quality levels; your Node process loses if you recompress every dynamic response on the hot path while CloudFront could have cached Content-Encoding. The senior move is to compress at the right layer — build time for assets, CDN for cacheable responses, Node only for truly dynamic bodies — and to pick quality levels from measured CPU, not blog defaults.
⚡ TL;DR: Pre-compress static assets with Brotli+Gzip at build; let CloudFront serve encoded variants; in Node use Gzip level 4–6 for dynamic JSON unless bodies are large and CPU headroom is proven for Brotli q=4; never double-compress. Pair with Lambda Warm Pools thinking: cold CPU budgets hate surprise compressors.
Where compression should live
| Payload | Best layer | Encoding |
|---|---|---|
| JS/CSS/HTML assets | CI + CDN | br + gzip files |
| Cacheable API GET | CDN | br/gzip at edge |
| Dynamic personalized JSON | Node (careful) | gzip default |
| Already compressed (images, zip) | nowhere | identity |
// src/compress.ts
import { gzipSync, brotliCompressSync, constants } from "node:zlib";
export function encodeBody(buf: Buffer, accept: string | undefined, kind: "static" | "dynamic") {
if (buf.length < 1024) return { body: buf, encoding: undefined as string | undefined };
const wantsBr = /\bbr\b/.test(accept ?? "");
const wantsGz = /\bgzip\b/.test(accept ?? "");
if (kind === "static" && wantsBr) {
// ✅ Higher quality OK offline
return {
body: brotliCompressSync(buf, {
params: { [constants.BROTLI_PARAM_QUALITY]: 11 },
}),
encoding: "br",
};
}
if (wantsBr && kind === "dynamic" && buf.length > 16_384) {
// ✅ Lower quality on request path
return {
body: brotliCompressSync(buf, {
params: { [constants.BROTLI_PARAM_QUALITY]: 4 },
}),
encoding: "br",
};
}
if (wantsGz) {
return { body: gzipSync(buf, { level: 5 }), encoding: "gzip" };
}
return { body: buf, encoding: undefined };
}
// ❌ Brotli quality 11 inside the request handler for every JSON response
app.use(compress({ brotli: { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11 } } }));
Measure encode cost vs transfer savings
Instrument CPU time and bytes_out. A 8% size win that burns 25ms of event-loop time on p99 is a regression for APIs — and invisible if you only watch average bandwidth.
import { performance } from "node:perf_hooks";
export function timedEncode(fn: () => Buffer) {
const t0 = performance.now();
const body = fn();
metrics.timing("compress.encode_ms", performance.now() - t0);
metrics.histogram("compress.bytes", body.length);
return body;
}
CloudFront and Accept-Encoding
Configure the CDN to respect Accept-Encoding and cache encoded variants. If Node compresses and the CDN caches without varying on encoding, clients get wrong encodings. Prefer origin response policies that already compress at the edge for cache hits so Node sees fewer compressions.
# ✅ Build-time assets
brotli -q 11 -k dist/assets/*.js
gzip -9 -k dist/assets/*.js
# Upload .br/.gz alongside originals; set Content-Encoding + Vary
Streaming compression
For large dynamic streams, use zlib transforms and honor backpressure — same discipline as Zero-Copy Node Streams.
import { createGzip } from "node:zlib";
import { pipeline } from "node:stream/promises";
import { Readable } from "node:stream";
export async function streamGzip(res: NodeJS.WritableStream, src: Readable) {
res.setHeader?.("Content-Encoding", "gzip");
await pipeline(src, createGzip({ level: 5 }), res as any);
}
Closing checklist
✅ Dos
– ✅ Pre-compress static assets at build with br+gzip
– ✅ Prefer CDN compression for cacheable GETs
– ✅ Cap dynamic Brotli quality (≈4) or stick to Gzip 4–6
– ✅ Vary on Accept-Encoding correctly
– ✅ Meter encode_ms alongside egress bytes
❌ Don’ts
– ❌ Don’t Brotli-11 on the request path
– ❌ Don’t compress images/videos/zips
– ❌ Don’t compress bodies under ~1KB
– ❌ Don’t stack Node + CDN compression blindly
– ❌ Don’t ignore event-loop cost of zlib on p99
Related reading
- Zero-Copy Node Streams: Pipe Large S3 Objects Without Buffering
- Lambda Warm Pools: Low-Latency Backends for Coding Agent Tools
- Node.js Event Loop Lag: Catch P99 Stalls
- HTTP/2 in Node Behind ALB: Multiplexing Pitfalls That Reset Streams
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.

Pingback: Zero-Copy Node Streams: Pipe Large S3 Objects Without Buffering - CheatCoders