toolhq.io

All posts
6 min readby Jameel Haider

gzip vs brotli vs zstd for HTTP compression

Brotli usually beats gzip on text by 15 to 25 percent at comparable speed, which is why it has become the default for static web assets, but the right choice depends entirely on whether you are compressing a file once or compressing a response on every request. The ratio is only half the story.

How the negotiation works

The client advertises what it can decompress with Accept-Encoding, and the server replies with the one it chose in Content-Encoding.

Accept-Encoding: br, gzip
Content-Encoding: br

The server never compresses with something the client did not list, so a body comes back uncompressed if the client sent no Accept-Encoding. Always pair conditional compression with Vary: Accept-Encoding so caches do not serve a brotli body to a client that only understands gzip.

The three algorithms

AlgorithmEncoding tokenTypical text ratioCompress speedBest use
gzipgzipbaselinefastdynamic responses, universal support
brotlibr15–25% smaller than gzipslow at high levels, fast at lowstatic assets, text
zstdzstdnear brotlivery fastdynamic responses, newer clients

gzip is supported everywhere and has been for decades, so it is the safe fallback. Brotli is supported by every current browser over HTTPS. zstd is the newest and is gaining support, prized for compressing nearly as well as brotli while running much faster, which makes it attractive for content generated per request.

The ratio versus CPU trade

Compression is not one setting but a scale of levels. Brotli runs from level 1 to 11. Level 11 produces the smallest output but is slow enough that you would never run it on a live request. Level 4 or 5 is fast enough for dynamic content while still beating gzip. gzip runs 1 to 9 with a much flatter curve: the difference between its levels is small, so gzip at a middle level is a reasonable default when CPU matters.

The key insight is that compression cost is paid differently for static and dynamic content.

Static: compress once, serve forever

A CSS bundle, a JavaScript file, or a font is built once and served millions of times. The compression cost is paid a single time at build, so you should use the slowest, highest ratio setting available, brotli level 11. Precompress the files during your build and let the server send the stored .br file directly. Every byte saved there is a byte saved on every future request, and the CPU cost was spent once offline.

Dynamic: compress on every request

An API response or a server rendered page is generated fresh each time, so its compression cost is paid on every request under load. Running brotli 11 here would burn CPU and add latency to each response. Use a fast level: brotli 4 to 5, gzip 6, or zstd at a middle level. The goal is most of the size benefit at a fraction of the CPU, because the time spent compressing adds directly to response time.

When brotli wins and when it does not

Brotli wins clearly on text: HTML, CSS, JavaScript, JSON, and SVG. Its advantage comes partly from a built in dictionary of common web strings. On already compressed binary formats such as JPEG, PNG, WOFF2 fonts, or video, neither brotli nor gzip helps, and compressing them again wastes CPU while sometimes making the file slightly larger. Exclude those content types from compression entirely.

Quick guidance

  • Static text assets: brotli 11, precompressed at build time.
  • Dynamic responses: brotli 4 to 5 or zstd, never a high level.
  • Already compressed binaries: skip compression.
  • Always send Vary: Accept-Encoding.

To confirm which encoding a server actually returned and whether Vary is set, the HTTP Header Checker shows the Content-Encoding and related headers for any URL.