Compressing HTTP responses shrinks the bytes sent over the network — often by 70–90% for text — which means faster page loads, lower bandwidth bills, and better experience on slow connections. The browser advertises what it supports via Accept-Encoding, the server compresses with gzip, deflate, or the more efficient Brotli, and sets Content-Encoding so the browser decompresses transparently. This page covers when compression helps (and when it hurts), the algorithms, the Express compression middleware, the CPU trade-off, and why you often want a proxy or CDN to do it instead.
How content negotiation works
Text
Client request:
GET /api/data HTTP/1.1
Accept-Encoding: gzip, deflate, br ← "I can decompress these"
Server response:
HTTP/1.1 200 OK
Content-Encoding: br ← "I compressed with Brotli"
Vary: Accept-Encoding ← caches must key on encoding
...compressed bytes...
The browser advertises support; the server picks an algorithm and sets `Content-Encoding`
Compression is negotiated per request. The client sends `Accept-Encoding` listing the algorithms it understands; the server chooses one, compresses the body, and signals its choice with `Content-Encoding` so the browser knows to decompress. The `Vary: Accept-Encoding` response header is essential when responses are cached: it tells shared caches to store separate entries per encoding, so a client that doesn't support Brotli isn't served a Brotli-encoded body it can't read. All of this is transparent to your application code — you emit normal text, the transport layer handles the squeezing.
The algorithms
Algorithm
Token
Notes
gzip
gzip
Universal support, fast, good ratio — the safe default
Brotli
br
Better ratio than gzip (esp. text), slightly more CPU; broad modern support
deflate
deflate
Older; little reason to prefer over gzip
Brotli compresses text better than gzip; gzip is faster and universal — many setups offer both
**Brotli** typically achieves 15–25% smaller output than gzip on text (HTML, JSON, CSS, JS), making it the better choice for compressible content where the modest extra CPU is worth the bandwidth saved — especially for static assets you can compress *once* and serve many times. **gzip** is slightly faster and supported literally everywhere, making it the safe default for dynamic responses. A common production setup advertises both and lets the negotiation pick Brotli for modern clients, gzip otherwise. For pre-compressible static files, generate `.br` and `.gz` versions at build time so you never pay compression CPU at request time.
Only compress text — and skip small bodies where the overhead outweighs the saving
The `compression` middleware gzips responses automatically, but two settings matter. `threshold` skips tiny bodies (under ~1KB): compression has fixed overhead and headers, so squeezing a 200-byte JSON response can make it *bigger* and wastes CPU. The `filter` controls *what* gets compressed — by default text-based content types (HTML, JSON, CSS, JS, SVG). Critically, it skips already-compressed formats. Place the middleware early in the chain so it wraps the responses below it.
Don't compress already-compressed content
Compressing JPEGs, PNGs, MP4s, or ZIPs wastes CPU and can make them larger
Images (JPEG, PNG, WebP), video (MP4), audio (MP3), and archives (ZIP, gzip) are *already compressed* — running them through gzip/Brotli burns CPU to achieve essentially zero size reduction, and the compression overhead can even make the output marginally larger. Compress **text**: HTML, CSS, JavaScript, JSON, XML, SVG, plain text. The default `compression.filter` already excludes binary media by content type, so the main risk is overriding the filter carelessly. The rule: compressible = text; skip everything that's already in a compressed container format.
The CPU trade-off — and who should compress
Compression is CPU work on Node's single thread — for high traffic, let a proxy or CDN do it
Compression isn't free: it consumes CPU, and in Node that CPU is spent on the [single thread](/nodejs/event-loop) that also handles your request logic. At low-to-moderate traffic, in-process `compression` middleware is fine and the bandwidth win dominates. But at high throughput, compressing every dynamic response in Node can become a bottleneck that adds event-loop latency. The scalable answer is to **offload compression to a reverse proxy ([Nginx](/nodejs/reverse-proxy-nginx)) or CDN** in front of your app — they're optimized for it, can cache compressed results, and free your Node process to do application work. For static assets, pre-compress at build time so neither Node nor the proxy compresses on the fly.
Streaming and compression
Compression works with streaming responses — but interacts with flushing and `Content-Length`
When you stream a large response, compression middleware compresses the stream chunk-by-chunk, which keeps memory low and lets bytes start flowing before the whole body is ready. Two interactions to know: compressed responses generally can't send a `Content-Length` (the final size isn't known up front), so they use chunked transfer encoding; and if you need data to reach the client promptly (e.g. server-sent events), you may need to `res.flush()` so the compressor doesn't buffer waiting for more input. For ordinary buffered JSON/HTML responses none of this matters — it's only relevant for long-lived streams.
Next
Prove your optimizations hold under real traffic: [Load Testing](/nodejs/load-testing).