How to Enable Brotli on nginx and CDNs

This guide is part of Compression: Brotli & Zstandard, within Network & Server Response Optimization. Brotli typically compresses web text 15–25% better than gzip, and every modern browser supports it over HTTPS. Yet many sites still serve gzip, because Brotli is not enabled by default in nginx, needs a module on some platforms, is disabled for certain content types on CDNs, or is lost when an origin's Brotli response passes through a proxy that only knows gzip.

Enabling Brotli is mostly configuration, but the details matter: which MIME types to compress, which level to use for dynamic responses, whether to serve precompressed files, how to keep Vary: Accept-Encoding correct, and how to make sure the CDN does not decompress and recompress with gzip.

Transfer size of a news homepage (text resources) Bar chart comparing total transfer size of HTML, CSS and JavaScript on a homepage with gzip versus Brotli. Transfer size of a news homepage (text resources) gzip 6 (on the fly) 612KB Brotli 5 (on the fly) 528KB Brotli 11 static + 5 dynamic 489KB

Rapid Diagnosis

  • Check Content-Encoding on HTML, CSS and JS in a modern browser's Network panel.
  • Request with explicit headers: curl -sI -H 'Accept-Encoding: br' https://example.com/ and look for content-encoding: br.
  • Check the CDN dashboard for compression settings and supported content types.
  • Check origin vs edge: request the origin directly to see whether Brotli is lost at the CDN.

Root Cause Analysis

1. Not enabled. nginx needs the Brotli module; many default configs only enable gzip.

2. MIME types missing. JSON, SVG, XML or text/javascript omitted from the compressible list.

3. CDN normalisation. The CDN serves gzip because its compression setting or cache key does not include Brotli.

4. Proxies. Intermediate proxies strip or rewrite Accept-Encoding.

Step-by-Step Resolution

1. nginx with the Brotli module

nginx
# Requires ngx_brotli (packaged on many distributions, or built as a dynamic module).
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;
http {
  brotli on;
  brotli_comp_level 5;
  brotli_min_length 1024;
  brotli_types text/plain text/css text/javascript application/javascript application/json
               application/xml image/svg+xml application/manifest+json font/ttf font/otf;
  brotli_static on;                     # serve file.br when present
  gzip on; gzip_static on; gzip_vary on;
  gzip_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml;
}
# trade-off: brotli_comp_level 5 balances CPU and size for dynamic responses;
# static assets should be precompressed at 11 and served via brotli_static.

text/html is always compressed when compression is on and does not need listing.

2. Apache and Node.js

apache
# Apache (mod_brotli)
AddOutputFilterByType BROTLI_COMPRESS text/html text/css text/javascript application/javascript application/json image/svg+xml
BrotliCompressionQuality 5
javascript
// Node.js (Express) with the compression middleware supporting Brotli.
import compression from 'compression';
import { constants } from 'node:zlib';
app.use(compression({ brotli: { params: { [constants.BROTLI_PARAM_QUALITY]: 5 } }, threshold: 1024 }));
// trade-off: in-process compression uses the same CPU as rendering; at high load,
// offload compression to a reverse proxy or the CDN.

3. CDNs

Most CDNs support Brotli: some compress at the edge automatically for eligible content types; others pass through origin Brotli responses when the cache key includes the encoding. Enable Brotli in the CDN settings, check the list of compressible content types, and ensure the origin's precompressed Brotli responses are not decompressed.

4. Verify Vary and caching

Responses that vary by encoding must include Vary: Accept-Encoding (or the CDN must key by encoding), so caches never serve Brotli to clients that cannot decode it.

Brotli negotiation through a CDN Sequence showing a browser requesting Brotli, the CDN forwarding to the origin, and caching the Brotli variant. Brotli negotiation through a CDN Browser CDN edge Origin Accept-Encoding: br, gzip forward with br br body + Vary br response (cached per encoding)

Verification

bash
curl -sI -H 'Accept-Encoding: br, gzip' https://example.com/js/app.4f2a.js | grep -iE 'content-encoding|vary|content-length'
curl -sI -H 'Accept-Encoding: gzip' https://example.com/js/app.4f2a.js | grep -iE 'content-encoding'
# trade-off: HEAD requests may be answered differently by some servers; if in
# doubt, use GET with -o /dev/null and -D - to print headers.

The first should return br, the second gzip. In the browser, the Network panel's "Content-Encoding" column (right-click column headers to add it) should show br for text resources.

Worked Example: A Media Site on nginx Behind a CDN

A media site's nginx origin had Brotli enabled, but the CDN served gzip to every user. The CDN's cache key ignored Accept-Encoding, so it requested the origin with only gzip to stay safe. Switching on the CDN's Brotli support (which keyed variants by a normalised encoding value) made Brotli reach users. Text transfer per page view fell by 14%, and LCP p75 on mobile improved by about 120ms. A follow-up added brotli_static with precompressed assets, saving another 6% and reducing origin CPU.

Why am I still getting gzip? Decision sequence for diagnosing why a site serves gzip instead of Brotli. Why am I still getting gzip? Is the request over plain HTTP? Browsers only offer br over HTTPS yes no Does the origin return br when requested directly? Enable ngx_brotli or server support yes no Does the CDN return gzip for the same request? Enable CDN Brotli or pass-through yes no Is the content type in brotli_types? Add the missing MIME type yes no Check proxies that rewrite Accept-Encoding

Content Types People Forget

Default compression lists often miss: application/json (API responses), application/manifest+json (web app manifests), image/svg+xml (SVG icons and illustrations), application/xml and text/xml (sitemaps, feeds), text/javascript (the now-standard JavaScript MIME type), application/wasm (WebAssembly, which compresses moderately), and uncompressed font formats (font/ttf, font/otf). WOFF2 is already compressed and should be excluded. Check each content type your site serves against the list.

Testing Brotli Across the Delivery Chain

Brotli can be lost at any hop, so test each one. Request the origin directly (bypassing the CDN with its internal hostname or a hosts-file override) and confirm content-encoding: br. Then request through the CDN from a few locations. If an internal load balancer or ingress controller sits in front of the application, request through it too; some ingress controllers decompress and recompress or strip Accept-Encoding. Record the transferred size at each hop for one known asset; where it jumps, the hop is recompressing or not compressing.

Common Mistakes

  • Enabling Brotli at the origin but not the CDN. Users still receive gzip.
  • Using level 11 for dynamic responses. Adds latency to every request.
  • Missing Vary: Accept-Encoding. Risk of serving undecodable responses from caches.
  • Compressing WOFF2, images or video. No benefit, extra CPU.

Edge Cases

HTTP (not HTTPS). Browsers only advertise Brotli over HTTPS.

Old clients and bots. Keep gzip enabled for clients that do not support Brotli.

Server-Sent Events. Compression can buffer event streams; disable or configure flushing for SSE endpoints.

Range requests. Compressed responses and byte ranges interact poorly; serve large media uncompressed.

FAQ

Does nginx support Brotli natively?

Not in the core build. The ngx_brotli module adds it and is available as a package on many distributions or as a dynamic module.

What Brotli level should I use on nginx?

Around 4–6 for on-the-fly compression. Use brotli_static to serve precompressed level-11 files for static assets.

Will Brotli increase server CPU usage?

At moderate levels, slightly compared with gzip. Precompressing static assets removes most of the on-the-fly work.

Does my CDN need special settings for Brotli?

Usually an option to enable Brotli and a list of content types. Check whether it compresses at the edge, passes through origin Brotli, or both.

Should I disable gzip when Brotli is enabled?

No. Keep gzip for clients that do not support Brotli, such as some older browsers, tools and crawlers.

Is Brotli worth it for small responses?

Below about 1KB, savings are small and overhead can dominate. Set a minimum length.

How do I check Brotli in Chrome DevTools?

Add the Content-Encoding column in the Network panel, or select a request and check the Response Headers for content-encoding: br.

Does Brotli work with HTTP/3?

Yes. Content encoding is independent of the transport protocol, so Brotli responses work the same over HTTP/1.1, HTTP/2 and HTTP/3.

Can I enable Brotli only for some paths?

Yes. In nginx, the directives work per location, so you can enable Brotli for HTML and assets while leaving streaming endpoints untouched.