Brotli Compression This forces me to carefully weigh the trade-off between smaller transfer sizes and increased CPU usage. I’ll show how I usually achieve the best balance between time and size for dynamic responses using levels 4–6, and when levels 9–11 offer real advantages for pre-packed assets.
Key points
The following points provide me with a concise guide for planning and operations:
- Level Selection: Higher levels save bytes but use more CPU resources and take more time.
- Dynamics: For live compression, levels 4–6 often provide the best balance.
- Static: Pre-packaged assets benefit from levels 9–11.
- Comparison: Brotli often reduces file size more significantly, while Gzip compresses files faster.
- Operation: Metrics such as TTFB, CPU load, and error rate influence the choice.
Why the "Brotli" Level Matters
I'll decide that Compression Level Not based on gut feeling, but on cost and benefit. With each level, the computational load increases, while the additional byte savings become negligible at a certain point. This is exactly where the advantage tips: A file that’s a few percentage points smaller doesn’t always justify the increased latency and CPU load. Especially with live compression, setting the level too high slows down response times, even though the data transfer is reduced only minimally. I therefore use measurements and look at latency, processing time, and throughput before setting the level.
When I Choose Not to Compress
Not every byte saves time in a meaningful way. Very small responses (e.g., under 1–2 KB) and already compressed binary formats offer little benefit but consume CPU resources. That’s why I use Threshold values Per MIME type and route:
- Short text snippets or 204/304 responses: serve without compression.
- Images, videos, PDFs, archives: exclude them in general (they are often already compressed internally).
- Large Streaming Responses: Use Gzip or none at all to avoid latency spikes.
By clearly defining exclusions, I reduce the load on the workers and keep the P95/P99 TTFB stable.
Encoder Parameters That Make All the Difference
In addition to the quality level, the following factors influence Encoder Options Time and reason are palpable:
- Mode (generic, text, font): For HTML/CSS/JS, I set it to „text“; for fonts, I set it to „font.“ This helps the encoder recognize patterns more effectively.
- Window size (lgwin): Larger windows often improve readability for long pieces of content, but they consume RAM and CPU resources. I stick with the default settings for practical reasons and only increase the size for specific blocks of text.
- Block size: Blocks that are too small worsen the ratio, while blocks that are too large increase latency. I'm testing with representative payloads instead of applying a blanket tuning.
- Flush Strategy: Aggressive flushing reduces buffer latency but decreases compression. For APIs that use server streaming, I opt for a conservative flush frequency.
Dynamic Content: Sweet Spot 4–6
For HTML, JSON, or API responses, I compress them on the fly and pay close attention to Response time. Levels 4–6 usually offer the best balance of file size, CPU usage, and latency. This reduces the TTFB, keeps CPU utilization within reasonable limits, and increases the headroom during peak traffic. When I test higher levels, I often see increased CPU times without any noticeable benefit on the network. If you want to dive deeper, you’ll find plenty of practical details on CPU Load vs. Level, which illustrate precisely this compromise.
At Streaming (e.g., SSE or Chunked JSON), I sometimes skip Brotli or deliberately stick to lower levels. Reason: Brotli leverages context across longer sections; frequent flushing negates this advantage and drives up CPU usage. I therefore evaluate on a per-route basis whether throughput or latency is more important and whether microcaches can handle responses within a second.
Static Assets: Compress in Advance
For CSS, JavaScript, and other assets, I bundle them before serving them and accept higher computing time on the build server. Levels 9–11 work well here because the costs are incurred only once, and every additional savings counts in the long run. This is especially worthwhile for frequent downloads and on slow connections. I store the compressed artifacts alongside the originals and let the server deliver the correct format depending on the client. It’s important to plan for enough CPU and RAM during the build so that deployments run smoothly.
In the build, I define clear Exclusion Rules (e.g., no .jpg/.png/.mp4/.zip/.woff2), versioning, and cache busting via filenames. This keeps ETags consistent and prevents double compression. For large bundles, I split files when the application allows it; smaller, thematically sorted assets cache better and benefit disproportionately from the Brotli vocabulary.
Brotli vs. Gzip in Everyday Use
Text formats such as HTML, CSS, or JS tend to compress a bit more with Brotli, while Gzip often compresses faster and uses less CPU is needed. For live compression on high-traffic pages, I therefore keep Gzip as a fallback in case CPU spikes occur. For static assets, I prefer Brotli because the smaller file size makes a difference with every request. On older systems or with proxy chains, I remain flexible and support both formats. A good introduction to the direct comparison is provided by Brotli vs. Gzip with typical strengths and weaknesses.
What's important to me is the Capacity planning: If throughput (requests per second) is the metric, Gzip comes out on top when CPU resources are limited. If bandwidth or CDN egress is expensive, Brotli pays for itself very quickly when used for assets. That’s why I combine the two: Brotli as the default for static content, and Gzip as a flexible backup for live content.
CPU Budget, Latency, and TTFB
I first define a clear CPU Budget per request and use that as a guide for the level. This way, I prevent compression from dominating the TTFB or peak loads from causing errors. It’s helpful to categorize by use case, using relative effects rather than exact numbers. The following table shows how I match levels to scenarios. It’s no substitute for a benchmark, but it provides a reliable starting point for testing.
| Brotli Level | CPU/Time Requirements | Space savings | Suitable for | Note |
|---|---|---|---|---|
| 1-3 | low | moderate | Live Compression with Limited Resources | Fast, but fewer savings |
| 4-6 | medium | good | Dynamic HTML/API Responses | Often the Sweet spot for TTFB |
| 7–8 | increased | very good | Mixed scenarios, partly live, partly pre-recorded | Only when there is air in the CPU Budget |
| 9-11 | high | maximum | Pre-compressed static assets | Build Time Increases, Transfer Decreases |
Content Negotiation, Vary, and Cache Keys
To ensure that clients reliably receive the best option, I believe Content Negotiation clean:
- Vary: Accept-Encoding This is mandatory; otherwise, caches will return incorrect formats to downstream clients.
- Save the precompressed .br file next to the original file; the server will handle it correctly Content-Encoding: br and the right one Content type.
- When it comes to CDNs, I make sure that Cache keys „Take “Accept-Encoding" into account and cache Brotli and Gzip separately.
- I'll remain consistent with ETag/Last-Modified: Compressed and uncompressed artifacts will have their own validators to prevent mismatches.
I'm also testing how proxies and older HTTP/1.1 clients respond. When in doubt, I prioritize stability and either leave Gzip enabled or serve the content uncompressed.
Caching, Dictionaries, and Precompression
I reduce the load on the server by Caching compressed responses whenever the content allows. For recurring patterns in text, it’s worth looking into dictionaries, which increase the hit rate and reduce the time per request. When I use precompression, I ensure clean cache headers and filenames with extensions like .br so that the server can serve the content without re-encoding. For dynamic content, I check for edge caches or microcaches with runtimes measured in seconds, which significantly reduce load on hot paths. This way, I keep CPU usage predictable and ensure consistent response times.
Dictionaries I use this specifically when many responses contain similar tokens (e.g., namespaces, JSON keys). I keep the dictionaries small and version them so I can swap them out without downtime. For dynamic APIs, the margin of benefit is smaller, but it pays off when the traffic is homogeneous.
Configuration: Nginx, Apache, CDN
I enable Brotli specifically for each MIME type and block binary formats that rarely provide any benefit. On Nginx, I use the `map` directive to set different levels based on file size and path to conserve hot routes. I handle Apache similarly using filter chains and clear exceptions. For CDNs, I use precompression and Vary headers to ensure clients reliably receive the correct format. The guide to HTTP Compression with practical options.
In addition, I define a minimum size (min_length), at which point compression becomes active, and make sure that reverse proxies do not recompress the data. I can immediately detect double encoding by incorrect Content-Length headers or client errors. For Partial Content (Range Requests) I keep the original files on hand; compressed versions are only suitable to a limited extent and can confuse caches.
Monitoring and Benchmarks
I measure every change in the Levels using controlled benchmarks and production metrics. Key metrics include TTFB, throughput, CPU load per worker, and error rate under load. For dynamic routes, I test p95/p99 values because outliers significantly impact the user experience. I also compare the traffic mix and asset sizes before and after the migration to identify any side effects. Only when the values remain stable over several days do I declare the profile the new baseline.
My Test discipline In short:
- Use representative payloads (small/medium/large) and real headers.
- Perform a warm-up, then run the measurement window with a steady load.
- Monitor competing system factors (GC, I/O, TLS offload) separately.
- Always compare „like with like“: identical seeds, identical data sets.
Security and Edge Cases
Compression can facilitate side channels if secret tokens end up in reflected responses. I Disable compression on sensitive endpoints (login flows, CSRF tokens in HTML) or decouple them into separate routes. When there’s no other option, I reduce context (e.g., by using more neutral templates) to minimize data-dependent differences in length.
Other practical challenges:
- Damaged Artifacts Due to failed builds: Check checksums before deployment, and ensure correct file extensions (.br) and MIME types.
- Incompatible Proxies: Enable fallback to Gzip for unexplained 206/Content-Encoding errors.
- Timeouts If levels are high: Reduce the levels or increase worker/CPU quotas.
- Missing Vary Headers: Results in „incorrect“ responses in the CDN cache, which appear as display errors in certain browsers.
Priorities by Project Phase
In the early stages, I keep the difficulty level low to medium so that Iteration and keep deployments fast. As soon as traffic grows, I optimize static assets more aggressively and fine-tune dynamic responses to hit the sweet spot. When traffic spikes are imminent, I prefer to scale worker and cache capacities rather than recklessly increasing the level. For international audiences, I invest in precompression and edge caching because every millisecond counts on the web. This keeps the platform reliable without wasting resources.
WordPress and Hosting in Practice
In WordPress Stacks, I enable Brotli on the server side, not via Plugin in the PHP path to avoid CPU overhead. I have build pipelines pre-compress the assets and combine that with cache invalidation after deployment. Object cache and page cache further reduce dynamic compression. As a fallback, I keep Gzip enabled so that even exotic clients receive clean responses. If you’re planning to get started, you can use this practical guide as a reference and gradually move to higher levels as soon as the telemetry allows.
For multisite setups and headless themes, I believe pro-Route I have different profiles available: API routes at levels 4–5, HTML rendering paths at 5–6, and static bundles strictly pre-rendered at 10–11. It’s important that I properly map cache keys and purge logic to the new artifact names so that no outdated .br files remain in circulation.
Troubleshooting and Common Pitfalls
When something isn't running smoothly, I take a systematic approach:
- Double compression: Check whether the upstream (app server) is already compressing the data and the edge server is re-encoding it. Solution: Assign responsibility to only one party.
- Incorrect Content-Length: When using `Transfer-Encoding: chunked`, do not include a fixed length; otherwise, browsers will stop loading the page.
- Missing Originals: For range requests, legacy clients, and debugging, you must have uncompressed files available.
- Level is too aggressive: Symptoms include rising p99-TTFB, sporadic 5xx errors, and CPU saturation. Remedies: Lower the level or increase caching.
- Asset Mix Changed: After framework updates, token frequencies change—the ratio may suddenly worsen. Rebenchmark and adjust the dictionaries.
Briefly summarized
I choose the difficulty level deliberately and base it on tough Metrics. For dynamic content, I usually set the level to 4–6 because TTFB matters and CPU spikes can be costly. I pre-compress static assets at levels 9–11, since every additional percentage point of savings here has a multiplier effect. Brotli often delivers the best file sizes, while Gzip excels in terms of speed and as a fallback. Your own telemetry remains the deciding factor: by measuring and iterating, you’ll quickly find the right profile for your traffic, hardware, and user experience.


