For a long time, I treated compression as something the edge would handle for me. Cloudflare made that easy, and for many sites that is enough. The moment I started tightening my own Nginx and Hestia servers, I wanted the origin itself to prove smaller text responses, gzip fallback still alive, cache behavior unchanged, and WordPress assets still loading cleanly.
My test lane uses Ubuntu 22.04, Nginx, and the HestiaCP layout where global Nginx config and per-domain includes are separate. Newer Ubuntu releases provide Brotli module packages for the Ubuntu-packaged Nginx, so check the package path before compiling. The source-build path below remains useful on Ubuntu 22.04 and on custom or control-panel Nginx builds that need a module compiled against the exact running binary.
The Nginx path usually needs dynamic Brotli modules from ngx_brotli. The safe path is to build those modules against the exact Nginx version running on the server, load them, run nginx -t, then test with curl before touching cache or CDN settings. By the end, the server should prove that the modules load, the config test passes, and a request with Accept-Encoding: br returns content-encoding: br while gzip remains available for fallback.
What Brotli changes compared with gzip
Most web servers still use gzip compression because it is stable, fast, and supported everywhere. Brotli is the better test when a page sends large text assets, but I do not treat it as automatically better for every response. Compare the same HTML, CSS, and JavaScript files with gzip and Brotli, then trust the measured transfer size.
Text-heavy assets show the difference most clearly. I test theme CSS, plugin JavaScript, SVG, JSON, and long HTML responses. Images are not the target here because modern image formats already have their own compression. That is why I use real CSS, JavaScript, and HTML URLs instead of judging from the homepage alone.
One reason I like Brotli is the history behind it. Google introduced Brotli as an open-source general-purpose compressor, and the original Open Source announcement focused on smaller transfers for real users, including mobile users. The later Google Research paper describes Brotli as a general-purpose compressor designed for Internet content. I treat it as a server feature that must prove its value through headers and byte size, not as an SEO trick.
Checks that prove whether Brotli helps
After enabling Brotli, I check the response headers, the transferred bytes for the same URL with gzip and Brotli, and whether any cache layer changes the result. Smaller transferred bytes can help page speed, especially on slower networks, but Brotli will not fix render-blocking scripts, unused CSS, oversized images, or bloated theme output.
The first number I look at is transfer size, not a synthetic score. If Brotli saves a meaningful amount on the assets visitors actually download, it can help slower routes, mobile users, and repeated page views. If the file is already tiny or already well-compressed, the gain may be small, and that is fine too.
| Proof | Command or check | Expected proof |
|---|---|---|
| Nginx version | nginx -v |
The exact version used for the module build |
| Module path | nginx -V 2>&1 | grep modules-path |
The directory where the compiled modules belong |
| Config safety | nginx -t |
A clean test before any reload |
| Brotli response | curl -H "Accept-Encoding: br" -I URL |
content-encoding: br |
| Fallback response | curl -H "Accept-Encoding: gzip" -I URL |
content-encoding: gzip when Brotli is not requested |
| Transfer size | Compare the same URL with gzip and Brotli | Measured bytes, not a guessed percentage |
What you need before touching Nginx
Do this on a server where you are comfortable rebuilding an Nginx module and testing the config before reload. A small VPS can handle it, but the build step needs enough memory and temporary disk space to compile cleanly. If the server is already under pressure, take a backup or snapshot first and do the build during a quiet window.
You also need root access or a user with sudo privileges. The work touches system packages, Nginx module files, and Nginx configuration. If you are using HestiaCP, do not paste random config into every site file first. Get the module loaded globally, test Nginx, then decide where the per-site Brotli settings belong.
On a clean Ubuntu server, missing build packages are the most common early problem. That is normal. Install the compiler and development libraries first, then keep the terminal output visible until the module build finishes. If a package is missing, the error usually points close to the missing dependency.
A percentage from another server cannot tell me what my origin sends. I compare one HTML page, one CSS file, and one JavaScript file from the server I am changing. That small sample exposes whether Brotli changes the real transfer sizes before I spend time on broader tuning.
The most important detail is the exact Nginx version. Build the dynamic Brotli module against the same Nginx version that is running on the server. If those do not match, Nginx can refuse to load the module or fail its config test.
Step 1: Check the exact Nginx version
Start with the Nginx binary already serving the site. The version returned here decides which Nginx source archive you download later.
nginx -vExample output shape. Your installed version will differ.
nginx version: nginx/1.29.0Write down your own version number. Next, check where Nginx expects to find dynamic modules:
nginx -V 2>&1 | grep modules-pathExample output shape. Use the path returned by your own binary.
--modules-path=/usr/lib/nginx/modulesThat path is where the compiled Brotli module files need to land on your server.
Step 2: Install the build tools
If the server uses Ubuntu-packaged Nginx on Ubuntu 24.04 or later, check the distribution modules first. The packages libnginx-mod-http-brotli-filter and libnginx-mod-http-brotli-static can avoid a source build when their Nginx package matches the installed binary. Do not mix those modules into a custom Hestia or third-party Nginx build without proving binary compatibility.
For the Ubuntu 22.04 or custom-binary path in this guide, update the package list so Ubuntu installs current package metadata.
sudo apt updateThen install the compiler, Git, CMake, and the development libraries Nginx needs for the module build:
sudo apt install build-essential git gcc cmake libpcre3 libpcre3-dev zlib1g zlib1g-dev openssl libssl-devOn most clean Ubuntu 22.04 servers, those packages are enough to build the dynamic modules. If your server already has some of them, apt will skip what is installed. If apt reports a missing package, stop and fix that package issue before moving to the source download.
Step 3: Download and build the Brotli module
Move to a temporary directory for the build work. The commands below use $NGINX_VERSION, so set it from your live Nginx output first with NGINX_VERSION="$(nginx -v 2>&1 | cut -d/ -f2)".
cd /tmpDownload the Nginx source archive for that version. The source version must match the running Nginx version, because you are building a dynamic module for that binary.
wget https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz
tar -xzf nginx-$NGINX_VERSION.tar.gzThen download the Brotli module source with its submodules:
git clone --recursive https://github.com/google/ngx_brotli.gitEnter the Nginx source directory:
cd nginx-$NGINX_VERSIONConfigure the build to create dynamic modules without replacing your installed Nginx binary. This is the part I do not skip: the ngx_brotli README says dynamic modules must use the same configure arguments as the installed Nginx build, then append --with-compat and the Brotli dynamic module path. If you ignore that, Nginx can reject the module as not binary compatible.
NGINX_CONFIGURE_ARGS="$(nginx -V 2>&1 | sed -n 's/^configure arguments: //p')"
./configure $NGINX_CONFIGURE_ARGS --with-compat --add-dynamic-module=../ngx_brotliCompile only the module files:
make modulesProve the two build artifacts instead of matching a compiler transcript.
ls -lh objs/ngx_http_brotli_filter_module.so
objs/ngx_http_brotli_static_module.soThe build may take a few minutes. If it fails with missing library errors, do not force the next step. Install the missing development package, rerun the configure step, and build again.
If the Brotli repository did not pull its submodules cleanly, refresh them before building again:
cd ../ngx_brotli
git submodule update --init
cd ../nginx-$NGINX_VERSIONStep 4: Copy the compiled modules
Create the modules directory if it does not exist:
sudo mkdir -p /usr/lib/nginx/modulesCopy the two compiled module files into the Nginx modules directory, then confirm they are really there:
sudo cp objs/ngx_http_brotli_filter_module.so /usr/lib/nginx/modules/
sudo cp objs/ngx_http_brotli_static_module.so /usr/lib/nginx/modules/
sudo chmod 644 /usr/lib/nginx/modules/ngx_http_brotli_*.so
ls -la /usr/lib/nginx/modules/ngx_http_brotli_*.soIf both files are listed, the build produced the modules and Nginx can load them from the expected directory.
Step 5: Load the Brotli modules in Nginx
Open the main Nginx configuration file to load the dynamic modules. This part belongs in the main context, before the events and http blocks.
sudo nano /etc/nginx/nginx.confAdd the module load lines near the top of the file:
# Brotli modules
load_module /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so;
load_module /usr/lib/nginx/modules/ngx_http_brotli_static_module.so;If you are not using HestiaCP, you can enable Brotli in the http block or in the site config. Do not remove gzip yet. Keep gzip available as the fallback path while you prove Brotli works.
# Brotli compression
brotli on;
brotli_comp_level 6;
brotli_static on;
brotli_min_length 1024;
brotli_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json
image/svg+xml
font/ttf
font/otf
font/woff
font/woff2;
add_header Vary Accept-Encoding;Test the configuration first. Reload Nginx only if the test passes:
sudo nginx -t
sudo systemctl reload nginxStep 6: Add the HestiaCP site config
For HestiaCP, keep the module load lines in the main Nginx config, then put the site-level Brotli directives in a custom include for the domain. Hestia can regenerate normal domain config files, so do not treat a generated domain file as the permanent owner. Start in the domain config directory:
cd /home/username/conf/web/example.com/For the HTTPS site, create or edit the SSL custom include file. If you intentionally serve plain HTTP too, mirror the needed setting in an HTTP custom include as well.
sudo nano nginx.ssl.conf_customAdd the Brotli site directives there:
# Enable brotli compression for this site
brotli on;
brotli_comp_level 6;
brotli_static on;
brotli_min_length 1024;
brotli_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json
image/svg+xml
font/ttf
font/otf
font/woff
font/woff2;
add_header Vary Accept-Encoding;The ownership split is straightforward. Main Nginx loads the modules, and the Hestia domain include enables compression for the site. Test the complete configuration before reloading Nginx.
sudo nginx -t
sudo systemctl reload nginxFinal step: prove Brotli is actually serving
Test the HTML response first. Replace example.com with your own domain:
curl -H "Accept-Encoding: br" -I https://example.com/Example response shape. Verify these headers on your own URL.
HTTP/2 200
server: nginx
content-encoding: br
vary: Accept-EncodingThe key line is content-encoding: br. If it is missing, Brotli is not serving that response yet. Do not move to cache tuning until this header is true on the exact URL you are testing.
Then test a real CSS or JavaScript file that is larger than your brotli_min_length value:
curl -H "Accept-Encoding: br" -I https://example.com/wp-content/themes/yourtheme/style.cssExample asset response shape. Verify it on the file you requested.
HTTP/2 200
content-type: text/css
content-encoding: br
vary: Accept-EncodingCache and CDN notes
Do not start with the CDN. Prove Brotli at the origin first, then test any cache or CDN layer separately. If you skip that order, you will not know whether the origin is wrong or the edge cache is hiding the real response.
If a CDN or reverse proxy is in front of Nginx, test both layers. First request the origin directly, then request the public URL through the edge. Compare content-encoding, vary, cache status headers, and transferred bytes.
If the edge layer compresses responses itself, avoid double-guessing the result. Pick one owner for the final public response, clear the cache after config changes, and test again with the same URL and the same Accept-Encoding header.
Troubleshoot module and response-header failures
Most failures come from a missing build dependency, a module in the wrong directory, or a version mismatch between Nginx and the compiled module.
When Nginx fails to start after adding the Brotli modules, check that the module files exist in the directory reported by nginx -V. Running ls -la /usr/lib/nginx/modules/ngx_http_brotli* should show both files when that is the configured module path. If they are missing, return to the build artifacts instead of reloading Nginx again.
If Nginx configuration tests fail after adding Brotli directives, verify that the load_module statements are in the main Nginx context, above the http block. Brotli directives such as brotli, brotli_static, brotli_types, and brotli_comp_level can be applied in http, server, or location context depending on whether you want global or site-level behavior.
When compression headers do not appear in HTTP responses, check several potential causes. First, verify that your test requests include the correct Accept-Encoding header. The file being tested must be larger than the minimum compression size (usually 1024 bytes) and must be listed in the brotli_types configuration.
What to prove before you call Brotli finished
The final check is not a score. It is a byte and header check against the real URL you care about. Test the uncompressed response, gzip, and Brotli with the same path so you can see whether Nginx is changing the transfer size and whether the expected encoding is returned.
curl -H "Accept-Encoding: identity" -o /dev/null -w 'identity code=%{http_code} bytes=%{size_download}
' https://example.com/
curl -H "Accept-Encoding: gzip" -o /dev/null -w 'gzip code=%{http_code} bytes=%{size_download}
' https://example.com/
curl -H "Accept-Encoding: br" -o /dev/null -w 'brotli code=%{http_code} bytes=%{size_download}
' https://example.com/If the Brotli request does not return content-encoding: br, the feature is not live for that response yet. On a recent Hestia and Nginx audit, I saw gzip configured and --with-compat available, but no Brotli module files loaded. That kind of check is useful because it stops you from writing a success story before the server proves it.
For Hestia servers, keep a small note of the running Nginx version, the module path from nginx -V, the domain include file you changed, and the exact URL you tested. That makes future Nginx updates safer because you know what must be rebuilt or rechecked.