11 views
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.
Brotli vs Gzip in Node: production guidance
| 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 |
typescript
// 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 };
}
typescript
// ❌ 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.typescript
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.
bash
# ✅ 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.typescript
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
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- Css Animation Performance: Performance-Optimized Motion for Modern UIs
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
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