Brotli vs Gzip in Node: CPU Tradeoffs at the CDN Edge

Brotli vs Gzip in Node: CPU Tradeoffs at the CDN Edge

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

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

1 Comment

Leave a Reply