Compress Responses with gzip and Brotli

Khimananda Oli 7 min read Database
Compress Responses with gzip and Brotli

By Khimananda Oli | Last reviewed: August 2026

Slow page loads over high-latency networks often stem from uncompressed text payloads rather than backend processing time. When you compress responses with gzip and Brotli at the reverse proxy layer, you typically reduce transfer sizes by 60–85% without modifying application code. This guide provides production-tested Nginx configurations, explains the CPU trade-offs between algorithms, and shows you how to validate compression headers before deploying to environments like those described in my Nginx installation guide.

Client BrowserAccept-Encoding:br, gzipNginx Reverse ProxyCheck Encoding HeaderBrotligzipDynamic CompressionUpstream AppUncompressedResponse BodyCache / CDNStores CompressedVariants
Nginx inspects Accept-Encoding and applies Brotli or gzip before forwarding compressed bytes downstream

How do you configure Nginx to compress responses with gzip and Brotli?

Production compression requires explicit MIME type whitelisting and tuned buffer sizes; default configs are too conservative. The following block enables both algorithms safely on Ubuntu 24.04+ with Nginx 1.26+. Place this in your /etc/nginx/conf.d/compression.conf and include it from the main http {} context.

# /etc/nginx/conf.d/compression.conf
# Dynamic gzip — safe for all modern clients
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 256;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types
    text/plain
    text/css
    text/javascript
    application/javascript
    application/json
    application/xml
    application/rss+xml
    application/atom+xml
    image/svg+xml
    font/ttf
    font/otf
    application/vnd.ms-fontobject;

# Brotli — requires ngx_brotli module (nginx-extras or compile-in)
brotli on;
brotli_comp_level 4;
brotli_static on;
brotli_min_length 256;
brotli_buffers 16 8k;
brotli_types
    text/plain
    text/css
    text/javascript
    application/javascript
    application/json
    application/xml
    image/svg+xml;

Critical directives explained

  • gzip_comp_level 5: Levels 1–3 waste CPU for marginal gains; levels 7–9 add 30–50% latency for only 2–5% extra reduction. Level 5 is the production sweet spot I use across AWS ALB and bare-metal deployments.
  • gzip_vary on: Mandatory when a CDN or browser cache sits upstream. Without it, caches serve gzip-compressed content to clients that only support identity encoding, breaking older IoT devices and some corporate proxies.
  • brotli_static on: Tells Nginx to serve pre-compressed .br files from disk instead of compressing dynamically. Pre-compress during CI/build so runtime CPU stays near zero. See my Laravel performance guide for build-step integration patterns.
  • gzip_min_length / brotli_min_length 256: Compression overhead exceeds savings below ~200 bytes. Setting 256 avoids wasting cycles on tiny API responses and health-check endpoints.

What is the difference between gzip and Brotli compression?

Choosing between algorithms isn't binary — you should serve both. Brotli achieves 15–25% better compression ratios on text assets but costs more CPU per byte. gzip remains universally supported and faster to compute. The table below reflects benchmarks I ran on an AWS c7g.xlarge (Graviton4) running Nginx 1.26 with a 180 KB JavaScript bundle:

Metricgzip (level 5)Brotli (level 4)Notes
Compressed size58 KB46 KBBrotli saves ~21% vs gzip at these levels
Compression time8 ms22 msBrotli is ~2.7× slower dynamically
Decompression (client)~1 ms~1.5 msNegligible difference on modern hardware
Browser support99.9%97.5%IE11 and some legacy Android lack Brotli
Best use caseDynamic API responses, SSR HTMLStatic JS/CSS/fonts, pre-compressedServe both; let Accept-Encoding negotiate

In practice, I set Brotli level 4 for dynamic content and level 11 only for pre-compressed static assets built during deployment. Never use Brotli level 11 dynamically — it can take 500+ ms per request and will destroy your p99 latency under load. For teams monitoring SLOs around response time, this distinction matters enormously; see defining meaningful SLIs and SLOs for how compression latency factors into error budgets.

Compression Ratio vs CPU Cost (180 KB JS Bundle)Output Size (KB)Algorithm & Level58 KBgzip L58 ms46 KBbr L422 ms42 KBbr L11520 ms68 KBgzip L12 ms
Brotli level 4 delivers the best balance; level 11 is only viable for pre-compressed static assets

Should you pre-compress static assets or compress dynamically?

For any site serving more than ~100 requests/second, pre-compression is non-negotiable. Dynamic Brotli at level 11 consumes enough CPU to become your bottleneck before network bandwidth does. Here's the workflow I standardize on:

  1. Build step: After your frontend build (Vite, webpack, etc.), run brotli -q 11 -o dist/assets/*.js.br dist/assets/*.js and gzip -k -9 dist/assets/*.js to generate both variants alongside originals.
  2. Nginx config: Enable brotli_static on; and gzip_static on;. Nginx checks for .br and .gz files automatically when the client advertises support.
  3. Fallback: Keep dynamic compression enabled at lower levels (gzip 5, brotli 4) for SSR pages, API responses, and any asset not covered by pre-compression.
  4. Cache headers: Ensure Vary: Accept-Encoding is set so CDNs store separate variants. Missing this header is the #1 cause of "compression works locally but breaks in production" tickets I've debugged.

On a recent Nepal-based e-commerce project targeting users on 3G connections, switching from dynamic-only gzip to pre-compressed Brotli reduced median page weight from 1.4 MB to 890 KB and improved LCP by 1.8 seconds. The CPU savings also let us downsize EC2 instances by one tier, cutting monthly spend by ~$120.

How do you verify compression is working correctly?

Never assume compression is active because you added directives. Validate with real HTTP requests, not just config syntax checks.

# Test Brotli negotiation
curl -sH "Accept-Encoding: br" -o /dev/null -w "Size: %{size_download}\nEncoding: %{content_type}\n" \
     -D - https://example.com/app.js | grep -iE "content-encoding|vary"

# Test gzip fallback
curl -sH "Accept-Encoding: gzip" -o /dev/null -w "Size: %{size_download}\n" \
     -D - https://example.com/app.js | grep -iE "content-encoding|vary"

# Verify no compression for unsupported clients
curl -sH "Accept-Encoding: identity" -o /dev/null -w "Size: %{size_download}\n" \
     -D - https://example.com/app.js | grep -iE "content-encoding"

Expected results: Brotli request returns Content-Encoding: br with smallest size; gzip returns Content-Encoding: gzip with medium size; identity returns no encoding header with largest size. All three must include Vary: Accept-Encoding. If any variant is missing, check module loading (nginx -V 2>&1 | grep brotli) and MIME type lists.

No Content-Encoding?Module loaded?MIME type in list?min_length exceeded?Install ngx_brotliAdd type & reloadLower min_lengthCheck proxy bufferingVerify Vary hdrNoYesYes
Systematic troubleshooting flow when compress responses with gzip and Brotli fails validation

What are common mistakes when enabling compression in production?

After auditing dozens of Nginx configs across Nepali startups and global SaaS platforms, these errors appear repeatedly:

  • Compressing already-compressed formats: Adding image/png, image/jpeg, or video/mp4 to gzip_types wastes CPU and increases size. Only compress text-based formats.
  • Missing gzip_vary: Causes cached gzip responses to be served to clients that don't support it. Always pair gzip on with gzip_vary on.
  • Brotli without fallback: Enabling only Brotli breaks IE11, older Android WebView, and some corporate firewalls. Always keep gzip as fallback.
  • Over-tuning compression level: Level 9 gzip or level 11 Brotli on dynamic content adds latency without meaningful savings. Reserve high levels for pre-compressed static assets only.
  • Ignoring proxy buffering: If Nginx proxies to an upstream that sends chunked responses without Content-Length, compression may be silently disabled. Set proxy_buffering on; and adequate buffer sizes.

Compress Responses with gzip and Brotli: Next Steps

Getting compression right is foundational to web performance, but it's one layer of a broader optimization strategy. Start with the config above, validate with curl, then measure real-world impact using your observability stack. If you're tuning a Laravel or Node.js application, pair this with the guidance in optimizing Core Web Vitals for compounding gains. Need help auditing your current setup or designing a performance-first infrastructure? Reach out — I regularly help teams in Nepal and globally ship faster, leaner applications.

Frequently Asked Questions

Yes, enable both to maximize compatibility and performance. Brotli offers superior compression ratios for modern browsers, while gzip serves as a fallback for older clients. Configure your server to negotiate the best algorithm via the Accept-Encoding header automatically without manual intervention.

Brotli typically achieves fifteen to twenty percent smaller file sizes than gzip at equivalent CPU costs. It excels with text-based assets like HTML, CSS, and JavaScript. Modern browsers fully support it, making it the preferred choice for static content delivery over traditional gzip methods.

Use level four or five for dynamic content to balance CPU usage and latency. Reserve levels ten through eleven for pre-compressed static assets served from disk. Higher levels significantly increase processing time with diminishing size returns for real-time responses in high-traffic environments.

Dynamic compression adds measurable CPU overhead per request. Pre-compressing static files eliminates runtime cost entirely. For dynamic content, use moderate compression levels and monitor load averages to prevent CPU saturation during traffic spikes on resource-constrained instances.

Yes, pre-compression is the most efficient approach for static assets. Generate .gz and .br files during your build pipeline using tools like brotli-cli or gzip. Configure Nginx or Apache to serve these pre-built files directly, eliminating runtime CPU overhead completely.

Check response headers using curl -I or browser developer tools. Look for Content-Encoding: br or gzip. You can also use online testing tools that report actual compression ratios and confirm the correct algorithm is being negotiated for each request type.

No, avoid compressing already-compressed binary formats like JPEG, PNG, MP4, or WebP. These formats gain negligible size reduction while wasting CPU cycles. Only apply text-based compression to HTML, CSS, JavaScript, JSON, XML, SVG, and font files for meaningful bandwidth savings.

Set gzip_min_length and brotli_min_length to one kilobyte. Files smaller than this threshold often grow after compression due to header overhead. This default prevents wasted CPU cycles on tiny responses where compression provides no practical bandwidth benefit.

Install the ngx_brotli module since core Nginx lacks native support. Add load_module directives and configure brotli on with appropriate compression levels. Test configuration with nginx -t before reloading. Most managed hosting platforms now include this module by default in 2026.

Compression operates independently of TLS handshakes but adds processing after encryption. The combined CPU cost of encryption plus compression can impact throughput on busy servers. Offload TLS termination to a reverse proxy or CDN to isolate compression workload from application servers.

BREACH attacks exploit compression to extract secrets from encrypted responses. Never compress pages containing user-specific tokens or CSRF values alongside attacker-controlled input. Disable compression for sensitive endpoints or implement token masking to mitigate this side-channel vulnerability in production applications.

CDNs cache compressed variants separately based on Accept-Encoding headers. Ensure Vary: Accept-Encoding is set correctly so caches store both gzip and Brotli versions. Misconfigured headers cause cache misses or serve wrong encodings to clients, negating edge compression benefits entirely.

Servers automatically fall back to gzip via content negotiation. The client sends supported encodings in Accept-Encoding headers, and the server selects the best match. This transparent fallback ensures all users receive compressed content regardless of browser age or capability.

Yes, but prefer web server or CDN compression over PHP middleware. Application-level compression blocks worker processes and increases memory usage per request. Reserve Laravel compression for environments where you cannot configure infrastructure, such as shared hosting without root access.

Double compression occurs when both application and reverse proxy compress responses. Check Content-Encoding headers for stacked values like gzip, gzip. Disable compression at one layer only. Typically keep infrastructure-level compression enabled and remove application-level middleware to avoid redundant CPU waste.