"Just enable gzip or brotli" undersells the decision
Most performance guides land on "minify your JSON and enable gzip or Brotli" and stop there, as if the two were interchangeable checkboxes. They compress similarly well on JSON in the common case, but they get there differently, and that difference matters most exactly where a lot of real APIs live: compressing a unique, dynamically generated response on every single request, not a static file served from cache.
How each algorithm actually works
Gzip is DEFLATE — LZ77 (finds and back-references repeated byte sequences within a sliding window) followed by Huffman coding (shorter codes for more frequent bytes). It's been the web standard since the 1990s, implemented everywhere, and its behavior is predictable: fast, moderate compression ratio, low CPU cost even at higher levels.
Brotli does the same LZ77 + Huffman combination, but adds two things gzip doesn't have: a much larger sliding window (up to 16MB vs. gzip's 32KB, letting it find repeated patterns much further apart in a large payload) and a built-in static dictionary of roughly 13,000 common strings and phrases pulled from real web content — HTML tags, common JavaScript/CSS tokens, and common words. That dictionary is why Brotli tends to win specifically on JSON API responses: repeated key names, common English words in string values, and JSON's own punctuation patterns overlap heavily with what the dictionary was built from, giving Brotli a head start gzip doesn't get.
The tradeoff nobody puts in the checklist
Brotli's compression ratio advantage is real, but it comes at a quality-level cost curve that's much steeper than gzip's. At maximum quality (11), Brotli produces noticeably smaller output than gzip — but takes meaningfully longer to compress, which is fine for a static asset compressed once at build time and served from cache thousands of times, and a real problem for a JSON API response generated fresh on every request, where that compression time is added to every single response's latency.
Static asset (compressed once, served many times):
→ Brotli quality 11 — maximize ratio, compression time doesn't matter
Dynamic API response (compressed fresh, every request):
→ Gzip level 6, or Brotli quality 4-5 — balance ratio against added latencyMost compression middleware defaults land here reasonably: Node's compression package uses gzip level 6 by default; nginx's brotli module defaults to a low-to-mid quality level specifically because it's usually applied to dynamic content. The mistake is copying a "brotli quality 11" config example meant for static assets onto a dynamic JSON API and being surprised by added response latency.
Content negotiation: the client decides, not the server
A server doesn't unilaterally pick one — the client advertises what it supports via the Accept-Encoding header, and the server picks the best one it also supports:
curl -H "Accept-Encoding: br, gzip" -I https://api.example.com/users
# Content-Encoding: br → server chose Brotli
# Content-Encoding: gzip → server chose gzip (older client, or br unsupported)// Express — both available, negotiated automatically per-request
import compression from "compression";
import shrinkRay from "shrink-ray-current"; // adds brotli support on top of gzip-only "compression"
app.use(shrinkRay()); // picks brotli when the client supports it, gzip otherwiseBrotli support is now essentially universal in modern browsers and is supported by every major CDN (Cloudflare, Fastly, CloudFront), but always keep gzip as the fallback — some non-browser HTTP clients, older proxies, and internal service-to-service callers still only send Accept-Encoding: gzip.