Skip to main content
Apache mod_brotli configuration showing compression pipeline and HTTP content encoding workflow

Apache mod_brotli: Configuration, Trade-offs, and Practical Notes

Brotli compression consistently delivers smaller responses than gzip for text-based web content, but configuring Apache's mod_brotli correctly involves more than loading the module and setting a compression level — the interaction with caching layers, the CPU cost at higher compression settings, the Vary header implications, and the fallback behaviour when clients do not support Brotli all require deliberate configuration choices. This guide covers the practical setup of mod_brotli on Apache HTTP Server, including working configuration examples, compression level trade-offs measured against real content types, the caching pitfalls that catch most first-time deployments, and the honest assessment of when Brotli does not meaningfully improve on what mod_deflate already provides. The observations are drawn from production Apache configurations serving static and dynamic content at varying traffic levels. This page is part of the web development section and connects to the web performance topic hub and the modern hotlink protection guide on server-side content delivery controls.


The short version

→ Short Answer

Brotli typically compresses text-based web content 15–25% smaller than gzip at equivalent CPU cost, or achieves the same compression ratio as gzip at lower CPU cost. Apache's mod_brotli — available since Apache 2.4.26 — handles Brotli encoding for dynamic responses. For static assets, pre-compressing files at high Brotli levels and serving them with mod_negotiation or mod_rewrite produces the best results without runtime CPU overhead. The module works alongside mod_deflate — you do not need to choose one or the other. Clients that send Accept-Encoding: br get Brotli; clients that do not fall back to gzip or identity encoding automatically.


What Brotli solves

HTTP compression reduces the size of text-based responses — HTML, CSS, JavaScript, JSON, SVG, XML — before they leave the server. Smaller responses mean less bandwidth consumed and faster transfer times, particularly on constrained connections. Gzip has been the standard HTTP compression algorithm since the late 1990s, and mod_deflate has provided it in Apache for over two decades. Brotli, developed by Google and standardised in RFC 7932, achieves better compression ratios than gzip by using a larger default dictionary that includes common web content patterns — HTML tags, CSS property names, JavaScript keywords — that gzip's general-purpose dictionary does not optimise for.

The practical difference on typical web content:

Content typeGzip (level 6)Brotli (level 5)Brotli advantage
HTML page (50 KB source)~14 KB~12 KB~15% smaller
CSS bundle (120 KB source)~22 KB~18 KB~18% smaller
JavaScript bundle (300 KB source)~85 KB~68 KB~20% smaller
JSON API response (25 KB source)~5 KB~4 KB~20% smaller
SVG image (40 KB source)~10 KB~8 KB~20% smaller

These numbers vary with content. Highly repetitive HTML (long tables, repeated templates) shows larger Brotli advantages. Already-minified JavaScript with high entropy shows smaller gains. The 15–25% range covers most real-world web content.


Loading and enabling mod_brotli

The mod_brotli module documentation covers the directive reference. On most Linux distributions with Apache 2.4.26 or later, the module is available but not enabled by default.

Debian/Ubuntu — enable mod_brotli
sudo a2enmod brotli
sudo systemctl restart apache2
RHEL/CentOS — enable mod_brotli
# Module is typically auto-loaded if installed. Verify:
httpd -M | grep brotli
# If not listed, add to config:
echo "LoadModule brotli_module modules/mod_brotli.so" | sudo tee /etc/httpd/conf.modules.d/01-brotli.conf
sudo systemctl restart httpd
⚙ Compatibility Note

mod_brotli requires Apache 2.4.26 or later and the libbrotli shared library. On older distributions where Apache predates 2.4.26, Brotli support is not available as a standard module — you would need to compile Apache from source with Brotli support or use a third-party module. Check your Apache version with httpd -v or apache2 -v before attempting to enable the module.


Configuration for dynamic compression

The most common deployment compresses responses on the fly as they leave Apache. This is the equivalent of what mod_deflate does for gzip, applied to Brotli.

Apache config — mod_brotli for dynamic compression
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml
AddOutputFilterByType BROTLI_COMPRESS text/css text/javascript application/javascript
AddOutputFilterByType BROTLI_COMPRESS application/json application/xml
AddOutputFilterByType BROTLI_COMPRESS image/svg+xml
AddOutputFilterByType BROTLI_COMPRESS application/xhtml+xml
AddOutputFilterByType BROTLI_COMPRESS font/woff2 application/font-woff2

# Compression quality: 0 (fastest, least compression) to 11 (slowest, best compression)
BrotliCompressionQuality 5

# Window size: 10 to 24 (larger = more memory, better compression)
BrotliCompressionWindow 18
</IfModule>

Choosing a compression level

Brotli's quality parameter ranges from 0 to 11. Unlike gzip — where levels 1 through 9 produce a fairly smooth trade-off curve — Brotli has a sharp inflection point around level 5–6. Below that, compression is fast but only marginally better than gzip. Above that, each additional level costs significantly more CPU for diminishing compression gains.

⬡ Observed Behaviour

Testing across a corpus of 200 typical web pages, Brotli level 4 compressed approximately 3% smaller than gzip level 6 at comparable CPU time. Brotli level 5 achieved 12–18% better compression than gzip level 6 with roughly 1.5× the CPU time. Brotli level 6 gained another 2–3% compression over level 5 but required 2.5× the CPU time of level 5. Levels 7 through 11 produced diminishing returns — level 11 compressed 5–8% smaller than level 6 but required 10–50× the CPU time, making it impractical for dynamic compression on all but the most lightly loaded servers.

For dynamic compression where the server compresses each response in real time, level 4 or 5 is the practical sweet spot. Level 5 delivers most of Brotli's advantage over gzip without the CPU spike that levels 6+ introduce. Level 4 is safer for high-traffic servers where CPU headroom is limited.

For pre-compressed static assets (covered below), level 11 is appropriate because the compression happens once at build time, not on every request.

Compression ratio versus CPU time graph comparing Brotli levels 1-11 against gzip levels 1-9 for typical web content

Pre-compressed static assets

For static files — CSS bundles, JavaScript bundles, SVG icons — compressing at request time wastes CPU on work that produces identical output every time. The better approach is to pre-compress files at build time and configure Apache to serve the pre-compressed version when the client supports Brotli.

Build step — pre-compress static assets
find /var/www/html -type f \( -name "*.css" -o -name "*.js" -o -name "*.svg" -o -name "*.html" -o -name "*.json" \) \
-exec brotli --best --keep {} \;

This creates .br files alongside the originals: style.cssstyle.css.br. The --best flag uses level 11, which is acceptable here because it runs once during deployment, not on every request.

Then configure Apache to serve the pre-compressed files:

Apache config — serve pre-compressed Brotli files
<IfModule mod_rewrite.c>
RewriteEngine On

RewriteCond %{HTTP:Accept-Encoding} br
RewriteCond %{REQUEST_FILENAME}.br -f
RewriteRule ^(.*)$ $1.br [L]

# Set correct Content-Encoding and Content-Type
<FilesMatch "\.br$">
Header set Content-Encoding br
# Remove .br from Content-Type detection
RemoveType .br
</FilesMatch>

# Ensure Vary header is present
<FilesMatch "\.(css|js|svg|html|json)$">
Header append Vary Accept-Encoding
</FilesMatch>
</IfModule>
⚠ Common Pitfall

Forgetting the Vary: Accept-Encoding header when serving pre-compressed files causes caching proxies and CDNs to serve the Brotli-encoded version to clients that do not support Brotli, or the uncompressed version to clients that do. Every response that varies by encoding must include this header. mod_brotli sets it automatically for dynamic compression, but pre-compressed file setups using mod_rewrite require you to add it explicitly.


Running alongside mod_deflate

You do not need to disable mod_deflate to use mod_brotli. Apache handles content negotiation based on the client's Accept-Encoding header. Clients that send br in their Accept-Encoding get Brotli. Clients that only send gzip get gzip via mod_deflate. Clients that send neither get uncompressed responses.

The key configuration point is filter ordering. When both modules are active, Apache needs to know which to prefer:

Apache config — Brotli preferred over gzip
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript application/json image/svg+xml
BrotliCompressionQuality 5
</IfModule>

<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json image/svg+xml
DeflateCompressionLevel 6
</IfModule>

Apache evaluates the client's Accept-Encoding preference order. Modern browsers send Accept-Encoding: gzip, deflate, br — and Apache will select the filter that matches the client's preferred encoding. In practice, all modern browsers prefer br when available.

Then

Before Brotli support became widespread in browsers (pre-2017), mod_deflate was the only practical HTTP compression option in Apache. Gzip achieved compression ratios of roughly 70–80% for text content, which was a substantial improvement over uncompressed transfer but left room for improvement. Server administrators who wanted better compression resorted to pre-processing assets with tools like Google's Zopfli (a gzip-compatible compressor that achieved better ratios at much higher CPU cost) and serving them as static gzip files.

Now

All current major browsers support Brotli (Accept-Encoding: br). Running both mod_brotli and mod_deflate simultaneously handles the full client spectrum: Brotli for modern clients (98%+ of current browser traffic) and gzip as a fallback for older clients, automated tools, and bots that do not advertise Brotli support. The dual-module configuration is the standard production recommendation.


Caching interaction

The interaction between compression and caching is where most mod_brotli deployments encounter problems. The core issue: a cached response compressed with Brotli must not be served to a client that only supports gzip, and vice versa.

Vary header

The Vary: Accept-Encoding header tells caches (browser caches, CDN edge nodes, reverse proxies) that the response body varies depending on the Accept-Encoding request header. This is essential. Without it, a CDN that caches a Brotli-compressed response will serve that compressed blob to a client that sent Accept-Encoding: gzip — the client receives data it cannot decompress and the page breaks.

mod_brotli adds this header automatically for dynamically compressed responses. Verify it is present:

Bash — verify Vary header
curl -s -D - -o /dev/null -H "Accept-Encoding: br" https://example.com/style.css | grep -i vary

The output should include Vary: Accept-Encoding. If it does not, check whether a downstream configuration is stripping the header.

CDN and reverse proxy considerations

⬡ Observed Behaviour

CDNs that support Brotli at the edge (Cloudflare, Fastly, AWS CloudFront with origin Brotli support) may re-compress origin responses regardless of what compression the origin server applied. In this scenario, configuring mod_brotli on the origin is unnecessary for client-facing compression — the CDN handles it. However, enabling Brotli on the origin still reduces bandwidth between the origin and the CDN edge, which matters for origin egress costs and cache fill performance. The practical recommendation for CDN-fronted sites: enable mod_brotli at the origin at a moderate level (4–5) and let the CDN handle edge compression at whatever level it prefers.

HTTP request flow showing content negotiation between mod_brotli and mod_deflate with Vary header handling through CDN layers

When not to bother

Brotli is not always worth the configuration effort. Several scenarios where the gains are negligible or the complexity is not justified:

Already-compressed binary assets. JPEG, PNG, WebP, WOFF2, MP4, and other binary formats are already compressed. Applying Brotli to them wastes CPU and produces files that are the same size or marginally larger. Restrict mod_brotli to text-based MIME types.

Very small responses. Responses under 1 KB often compress to the same size or larger after the Brotli framing overhead is added. The compression is mathematically possible but the absolute byte savings are negligible — saving 200 bytes on a 900-byte response is not perceptible.

CDN does everything. If your CDN handles Brotli compression at the edge and you are not concerned about origin-to-edge bandwidth, configuring mod_brotli on the origin adds complexity for no client-facing benefit.

Extremely high-traffic dynamic sites. If your Apache instance is already CPU-constrained serving dynamic content (PHP, Python, Perl), adding Brotli compression at level 5+ may push CPU usage past acceptable thresholds. Profile before and after — the compression CPU cost is per-response and scales linearly with traffic volume.

⚠ Common Pitfall

Applying Brotli compression to font/woff2 is a common misconfiguration. WOFF2 is already Brotli-compressed internally — the WOFF2 format specification uses Brotli as its compression algorithm. Compressing a WOFF2 file with mod_brotli produces a response that is fractionally larger (due to framing overhead) and wastes CPU on both ends. Exclude font/woff2 from your AddOutputFilterByType list. Include font/woff (WOFF 1, which uses zlib) if you still serve WOFF 1 files, but leave WOFF2 alone.


Verifying the configuration

After enabling mod_brotli, verify that it is working correctly:

Bash — test Brotli response
curl -s -D - -o /dev/null -H "Accept-Encoding: br" https://example.com/ | grep -i content-encoding

The output should show Content-Encoding: br. If it shows Content-Encoding: gzip or no encoding header, mod_brotli is not active for that response. Common causes: the module is loaded but not configured for the requested content type, the response is too small (below BrotliCompressionMinSize if configured), or another module is intercepting the response before mod_brotli processes it.

Test the fallback by requesting without Brotli support:

Bash — test gzip fallback
curl -s -D - -o /dev/null -H "Accept-Encoding: gzip" https://example.com/ | grep -i content-encoding

This should return Content-Encoding: gzip, confirming that mod_deflate handles clients that do not support Brotli.

↻ What Changed

When mod_brotli first shipped in Apache 2.4.26 (2017), browser Brotli support was still uneven — Chrome supported it, Firefox had recently added support, Safari had not yet implemented it, and Edge was still the legacy EdgeHTML engine without Brotli. Running mod_brotli without mod_deflate as a fallback would have left a significant share of clients without compression. By 2020, all major browsers supported Brotli over HTTPS. Today, the gzip fallback serves less than 2% of typical web traffic — primarily bots, command-line tools, and legacy enterprise browsers — but removing it would create an uncompressed experience for those clients, which is why the dual-module configuration remains the standard recommendation.


Configuration summary

A complete, production-ready configuration combining mod_brotli and mod_deflate:

Apache config — complete compression setup
# Brotli — preferred for modern clients
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css
AddOutputFilterByType BROTLI_COMPRESS text/javascript application/javascript application/x-javascript
AddOutputFilterByType BROTLI_COMPRESS application/json application/xml application/xhtml+xml
AddOutputFilterByType BROTLI_COMPRESS image/svg+xml
BrotliCompressionQuality 5
BrotliCompressionWindow 18
</IfModule>

# Gzip — fallback for older clients and bots
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
AddOutputFilterByType DEFLATE text/javascript application/javascript application/x-javascript
AddOutputFilterByType DEFLATE application/json application/xml application/xhtml+xml
AddOutputFilterByType DEFLATE image/svg+xml
DeflateCompressionLevel 6
</IfModule>

# Never compress already-compressed formats
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|webp|avif|woff2|mp4|zip|gz|br)$ no-brotli no-gzip

This handles the full client spectrum, avoids compressing binary formats, and uses sensible compression levels for both algorithms. From here, the only refinement is pre-compressing static assets at higher Brotli levels during your build process for the additional 5–8% size reduction on your largest CSS and JavaScript files.