
Compression is one of the few speed optimizations that doesn’t require new hardware or an app rewrite. With the right config, you can cut CSS/JS/HTML transfer size by roughly 15–30% versus gzip alone. That’s an immediate win on mobile and congested Wi‑Fi.
This Nginx Brotli compression tutorial shows how to enable Brotli safely on a VPS, keep gzip as a fallback, and confirm your CDN and caches stay correct.
For predictable CPU headroom and stable latency, do this on a VPS where you control Nginx and its modules.
A HostMyCode VPS fits well because you can pick the distro, pin versions, and roll back quickly if a change behaves badly.
What you’ll build (and what you’ll avoid)
You’ll configure Nginx to:
- Serve
br(Brotli) to browsers that support it, andgzipto everything else. - Compress the right MIME types (HTML, JSON, CSS, JS, SVG), while skipping already-compressed binaries.
- Keep cache correctness using
Vary: Accept-Encodingand sane proxy defaults. - Use safe compression levels that don’t burn CPU under load.
You’ll also skip the common “compress everything” mistake. That approach wastes CPU on images and video.
And you won’t mix this with HTTP/3/QUIC. Use a separate change window for that.
Prerequisites and baseline checks
This tutorial assumes:
- Ubuntu 24.04 LTS or Debian 12/13 on a VPS or dedicated server
- Nginx 1.24+ (1.26 is common in 2026 repos)
- Root or sudo access
First, confirm your Nginx version and build flags:
nginx -v
nginx -V 2>&1 | tr -- ' ' '\n' | egrep 'nginx/|--with-|--add-module'
Next, capture a baseline for one representative HTML page and one static asset (CSS/JS). Treat these headers as your before/after proof.
curl -sI https://example.com/ | egrep -i 'content-encoding|vary|content-type|content-length'
curl -sI https://example.com/assets/app.css | egrep -i 'content-encoding|vary|content-type|content-length'
If TLS is flaky right now, fix that before you tune performance.
Use TLS certificate renewal troubleshooting to get renewals and certificate chains clean.
Choose your Brotli path: packaged module vs building from source
In 2026 you usually have two realistic paths:
- Packaged dynamic module: quickest to deploy and easiest to maintain, if your distro repo provides
ngx_brotli(often available on Debian-based module packages). - Build Nginx with Brotli module: maximum control, but every Nginx upgrade becomes a rebuild (or you pin versions and accept slower patching).
For most operators, dynamic modules are the better trade. You can apply security updates without recompiling.
If your environment is locked down (compliance or a custom Nginx build), compiling is fine. Treat it like a documented internal package.
Option A (recommended): Install Brotli as an Nginx dynamic module
On Ubuntu/Debian, start by checking whether your repo offers a Brotli module package. Names vary, so search first:
sudo apt update
apt-cache search brotli | egrep -i 'nginx|ngx'
If you see a package such as libnginx-mod-brotli or nginx-module-brotli, install it:
sudo apt install -y libnginx-mod-brotli
Confirm the module files exist. The path can vary by distro:
ls -al /usr/lib/nginx/modules/ | egrep -i 'brotli'
Enable the module by loading it near the top of your main config. On many systems, that means /etc/nginx/nginx.conf before the events block:
sudo nano /etc/nginx/nginx.conf
Add (or verify) lines like:
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;
Then test and reload:
sudo nginx -t
sudo systemctl reload nginx
Option B: Build Nginx with ngx_brotli (only if you must)
If you can’t get a module package, you can compile Nginx with ngx_brotli.
Be honest about the operational cost. You’ll need to track updates and rebuild.
In production, pin the Nginx version to avoid surprise breakage.
sudo apt update
sudo apt install -y build-essential git zlib1g-dev libpcre3-dev libssl-dev
Download Nginx source that matches your current Nginx major/minor (or upgrade intentionally). Then fetch ngx_brotli:
cd /usr/local/src
sudo git clone --recursive https://github.com/google/ngx_brotli.git
# Download nginx source tarball to /usr/local/src and extract
Configure Nginx with --add-module=/usr/local/src/ngx_brotli. The exact build flow depends on how your distro packages Nginx.
Treat this as a custom build and document it internally.
Configure Brotli + gzip safely in Nginx
Put compression settings in one dedicated file instead of scattering them across server blocks.
A simple, maintainable pattern is /etc/nginx/conf.d/compression.conf (or an include under /etc/nginx/snippets/).
sudo nano /etc/nginx/conf.d/compression.conf
Use a “Brotli first, gzip fallback” setup:
# /etc/nginx/conf.d/compression.conf
# Always vary by encoding so caches keep separate objects.
# Nginx sets Vary automatically for gzip when gzip_vary is on.
# For Brotli, we set it explicitly.
add_header Vary "Accept-Encoding" always;
# --- Brotli ---
brotli on;
# 4–6 is a good hosting default. Higher levels add CPU cost fast.
brotli_comp_level 5;
# Only compress responses above this size
brotli_min_length 1024;
brotli_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/x-javascript
application/json
application/xml
application/xml+rss
image/svg+xml
application/vnd.ms-fontobject
font/ttf
font/otf
font/woff
font/woff2;
# --- gzip fallback ---
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_vary on;
# Avoid compressing proxied responses that are already compressed
gzip_proxied any;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/x-javascript
application/json
application/xml
application/xml+rss
image/svg+xml;
Why these values hold up in production:
- Level 5 is a good balance. Levels 9–11 can chew CPU during burst traffic, especially with large JS bundles.
- min_length 1024 avoids spending cycles on tiny responses where headers dominate any savings.
- Types list focuses on text-based content where Brotli pays off. It intentionally skips JPEG/PNG/WebP/AVIF, MP4, ZIP, and PDF.
Validate and reload:
sudo nginx -t
sudo systemctl reload nginx
Test Brotli from the command line (and catch common mistakes)
You’re checking three things: Brotli works when requested, gzip works as fallback, and cache separation is correct.
1) Force Brotli:
curl -sI https://example.com/ -H 'Accept-Encoding: br' | egrep -i 'content-encoding|vary|content-type|content-length'
You should see:
Content-Encoding: brVary: Accept-Encoding
2) Force gzip:
curl -sI https://example.com/ -H 'Accept-Encoding: gzip' | egrep -i 'content-encoding|vary|content-type|content-length'
3) Force identity (no compression):
curl -sI https://example.com/ -H 'Accept-Encoding: identity' | egrep -i 'content-encoding|vary|content-type|content-length'
If Brotli doesn’t show up, these are the usual causes:
- Module not loaded:
nginx -Vwon’t list dynamic modules. Verify the module files exist and yourload_modulelines are in place. - Types mismatch: if your HTML is
text/htmland you didn’t include it, it won’t compress. (Some distros add defaults; don’t rely on that.) - Proxy/CDN stripping Accept-Encoding: confirm your CDN forwards the header, or accept that the CDN will handle compression instead.
Make caching safe: Vary, CDNs, and proxy layers
The fastest way to get “randomly broken” pages is to mix compression with shared caches that don’t separate variants.
The fix is simple and consistent.
- Always set
Vary: Accept-Encodingfor compressed content. Here it’s global viaadd_header ... always. - Avoid double-compression. If your CDN compresses at the edge, either disable origin compression for cacheable assets or configure the CDN to respect origin encoding.
- Keep Cache-Control predictable on assets. Fingerprinted files pair well with Brotli because the CDN can store one object per encoding variant.
If Nginx sits in front of Apache/cPanel, validate the full proxy chain.
That setup often affects real-world speed more than protocol tweaks.
See running Nginx in front of Apache/cPanel for a clean, debuggable layout.
Pre-compressed static .br files (optional, but excellent for busy sites)
On high-traffic sites, on-the-fly compression becomes steady background CPU work.
Pre-compressing assets during your build/deploy is usually faster and more predictable.
This requires the static module (ngx_http_brotli_static_module) and two changes:
- Generate
.bralongside.cssand.jsduring deploy. - Tell Nginx to prefer static Brotli files when available.
Add to your compression config:
brotli_static on;
Then generate files (example for a deployed directory):
sudo apt install -y brotli
cd /var/www/example.com/public
find assets -type f \( -name '*.css' -o -name '*.js' -o -name '*.svg' \) -size +1k -print0 \
| xargs -0 -I{} brotli -f -q 5 {}
You’ll end up with app.css.br next to app.css. Nginx will serve the .br variant automatically to Brotli-capable clients.
Practical tip: reserve this for stable, versioned assets (bundles with fingerprinted names).
Avoid pre-compressing frequently changing HTML unless your invalidation workflow is airtight.
Tune compression levels for VPS reality (CPU, latency, and burst traffic)
Compression costs CPU. On a small VPS, that can mean smooth performance or latency spikes during a burst.
A quick tuning loop:
- Start with Brotli level 5 and gzip level 5 (as shown).
- Load-test a representative page for 3–5 minutes.
- If CPU climbs too high, drop Brotli to 4 before you touch gzip. For most hosting workloads, Brotli 3–5 is the sweet spot.
Quick diagnostics during a spike:
uptime
top -o %CPU
sudo ss -s
sudo journalctl -u nginx --since "10 min ago" --no-pager | tail -n 80
If you want proper alerting (disk, load, service checks), set it up once and stop guessing.
Use this monitoring tutorial and pick CPU thresholds that match your instance size.
WordPress-specific notes (what helps, what backfires)
On WordPress, compression typically helps most on:
- Theme CSS and JS bundles
- Block editor assets (JS-heavy)
- REST API responses (
application/json)
Where it can cause trouble:
- Admin/preview pages behind authentication if you have aggressive caching layers. If you see odd behavior, confirm you aren’t caching authenticated responses at the proxy/CDN.
- Already-minified, tiny files. They rarely shrink much and still cost CPU. Your
min_lengthsetting avoids most of this.
If you want WordPress kept patched with a tuned stack—without babysitting the server—consider HostMyCode WordPress hosting. You still get the speed benefits, with less operational overhead.
Checklist: production-ready Brotli rollout
- Back up your Nginx configs (at least
/etc/nginx) before edits. - Enable Brotli + gzip with conservative levels (4–6).
- Confirm
Vary: Accept-Encodingis present on compressed responses. - Verify HTML, CSS, JS, JSON compress correctly using
curl -I. - Watch CPU and latency during a real traffic window (or a synthetic test).
- If you have a CDN, confirm it stores separate variants per encoding.
- Optionally pre-compress stable assets and enable
brotli_static on;.
Troubleshooting: quick fixes for the problems you’ll actually see
Brotli never shows up (only gzip)
- Confirm browser sends
Accept-Encoding: br(curl test above). - Check your module load lines and module files in
/usr/lib/nginx/modules/. - Ensure your response
Content-Typematches one of yourbrotli_types.
Some assets download corrupted or “randomly” fail
- Confirm
Vary: Accept-Encodingis present. - If you’re behind a proxy/CDN, purge cache and test with cache bypass headers if available.
- Look for intermediaries that normalize or remove
Accept-Encoding.
CPU spikes after enabling compression
- Drop
brotli_comp_levelfrom 5 to 4, then re-check CPU. - Increase
brotli_min_lengthto 2048 to skip smaller objects. - Pre-compress static assets and enable
brotli_static on;for heavy bundles.
You’re unsure whether your TLS/redirect chain is slowing things down too
Compression won’t paper over a broken redirect chain or handshake problems.
If you’re seeing inconsistent SSL errors or chain warnings, use this TLS handshake troubleshooting guide to fix the fundamentals first.
Summary: the “good defaults” you can run for years
Brotli is still a straightforward win in 2026: smaller payloads, faster page loads, and less bandwidth burned on repeat visits.
Keep settings conservative, make Vary non-negotiable, and move to pre-compressed assets once traffic makes the CPU savings worthwhile.
If you want the control to tune Nginx at this level (and avoid noisy-neighbor CPU contention), start with a HostMyCode VPS.
If you’d rather not own patching and routine maintenance, managed VPS hosting keeps performance work like this from turning into a weekend chore.
If you’re enabling Brotli because your pages are getting heavier, you’re already thinking like a VPS operator. A HostMyCode VPS gives you full control over Nginx modules and caching behavior. If you want the same performance gains without day-to-day server administration, managed VPS hosting is the cleaner option.
FAQ
Should I disable gzip if Brotli is enabled?
No. Keep gzip as a fallback. Some clients, tools, and edge cases still prefer gzip, and the compatibility cost of removing it isn’t worth it.
What Brotli compression level is best for a typical VPS?
Start at 4 or 5. Level 6 can be fine on larger instances, but levels above that rarely justify the extra CPU for hosting workloads.
Do I need Brotli for WordPress if I already use a CDN?
Maybe. Many CDNs can compress at the edge. If your CDN already serves Brotli, you can keep origin compression minimal and focus on correct caching and headers.
Can Brotli break caching?
Yes, if you don’t vary by Accept-Encoding. Ensure Vary: Accept-Encoding is present so caches store separate objects for br and gzip.
How do I confirm my server is really serving Brotli?
Use curl -I with Accept-Encoding: br and check for Content-Encoding: br. Also validate with browser DevTools (Network tab) for real asset requests.