How to Enable Gzip Compression in Nginx

How to Enable Gzip Compression in Nginx

How to Enable Gzip Compression in Nginx

Gzip compression is one of those settings that takes about five minutes to configure and can cut your page transfer sizes by 60-80% for text-based content. I’ve never come across a legitimate reason not to enable it on a production server, yet I still regularly find fresh installs where it’s either off entirely or configured so narrowly that it’s barely doing anything. This guide walks through exactly how I configure it, what to compress and what to skip, testing, and the performance and security nuances worth knowing.

What Gzip Compression Actually Does

Gzip is a compression algorithm that Nginx can apply to a response body before sending it to the client, as long as the client indicates (via the Accept-Encoding: gzip request header) that it can decompress gzip content — which virtually every browser and HTTP client does by default today.

The server compresses the response, sends it with Content-Encoding: gzip, and the browser decompresses it transparently. The user never sees this happening — they just get a faster page load because far fewer bytes traveled over the network.

This works best on text-based content — HTML, CSS, JavaScript, JSON, XML, SVG — where repeated patterns compress well. It does very little (and can even hurt) for already-compressed binary formats like JPEG, PNG, MP4, or ZIP files, since those formats are already near their entropy limit.

Prerequisites

Check whether the gzip module is compiled in (it is by default in virtually all standard Nginx builds, including the ones from official repos):

nginx -V 2>&1 | grep -o with-http_gzip_static_module

If that returns with-http_gzip_static_module, you also have the static gzip module available, which I’ll cover below.

Step-by-Step Configuration

Step 1: Locate the Right Config Block

I add gzip settings inside the http block in /etc/nginx/nginx.conf, so they apply globally across all sites on the server:

sudo nano /etc/nginx/nginx.conf

Step 2: Enable Basic Gzip

http {
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;

    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/javascript
        application/x-javascript
        application/xml
        application/xml+rss
        application/json
        application/vnd.ms-fontobject
        application/x-font-ttf
        font/opentype
        image/svg+xml
        image/x-icon;
}

Let me explain each directive, because the defaults people copy-paste often skip important ones:

Step 3: Test the Config and Reload

sudo nginx -t
sudo systemctl reload nginx

Step 4: Verify Compression Is Actually Happening

curl -H "Accept-Encoding: gzip" -I https://example.com/style.css

Look for:

Content-Encoding: gzip
Vary: Accept-Encoding

If those headers aren’t present, something in the config isn’t matching — check the troubleshooting section below.

Should You Compress Everything? (gzip_min_length and gzip_disable)

Two more directives I always include:

gzip_min_length 256;
gzip_disable "msie6";

Static Gzip: Pre-Compressing Files

If your site serves the same static assets to many visitors (a CSS or JS bundle, for example), you can avoid compressing it fresh on every request by pre-compressing the file once and letting Nginx serve the pre-compressed version directly. This requires the gzip_static module (usually included in standard builds):

location ~* \.(css|js)$ {
    gzip_static on;
    expires 7d;
}

For this to work, you need a .gz version of each file sitting alongside the original:

gzip -k -9 /var/www/example.com/public/style.css

This creates style.css.gz next to style.css. When gzip_static on; is set, Nginx serves the pre-compressed .gz file directly to clients that support it, skipping the CPU cost of compressing on the fly entirely. This is a common step in modern frontend build pipelines (webpack, Vite, etc. can output .gz files automatically as part of the build).

A Complete Example Configuration

http {
    include mime.types;
    default_type application/octet-stream;

    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_min_length 256;
    gzip_disable "msie6";

    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/javascript
        application/x-javascript
        application/xml
        application/xml+rss
        application/json
        application/vnd.ms-fontobject
        application/x-font-ttf
        font/opentype
        image/svg+xml
        image/x-icon;

    server {
        listen 80;
        server_name example.com;
        root /var/www/example.com/public;

        location ~* \.(css|js)$ {
            gzip_static on;
            expires 7d;
            add_header Cache-Control "public";
        }

        location / {
            try_files $uri $uri/ =404;
        }
    }
}

Testing Gzip Thoroughly

Command-line verification, including size comparison:

curl -H "Accept-Encoding: gzip" -s -o /dev/null -w "%{size_download}\n" https://example.com/style.css
curl -s -o /dev/null -w "%{size_download}\n" https://example.com/style.css

Run the first with gzip requested and the second without, and compare the byte counts — the gzip version should be substantially smaller for a typical CSS or JS file.

Browser DevTools check. Open the Network tab, reload the page, and check the response headers for any text-based asset. Content-Encoding: gzip should be present, and the “Size” column typically shows both the transferred size and the actual (decompressed) size.

Online tools. Sites like GTmetrix or a simple “gzip test” search will tell you the same thing without needing curl, useful for a quick sanity check on a live domain.

Troubleshooting Common Issues

Gzip headers missing entirely. Check that the file’s MIME type is actually included in gzip_types. A common mistake is forgetting that gzip_types doesn’t include text/html twice — it’s on by default, but every other type needs to be explicit.

Gzip works for CSS/JS but not JSON API responses. If Nginx is proxying to a backend (Node, PHP-FPM, etc.), confirm gzip_proxied any; is set — without it, Nginx may not compress proxied responses at all, since the default value is off.

gzip_static not working — files served uncompressed even though .gz versions exist. Confirm the .gz file actually sits next to the original with a matching name, and that the module is compiled into your Nginx build (nginx -V | grep gzip_static). If it’s missing, you’ll need a build that includes it, or fall back to dynamic gzip on; compression instead.

Double compression / corrupted downloads. This happens if a backend application is already gzip-compressing its own output and Nginx tries to compress it again. Check backend framework settings — most frameworks that do their own compression set Content-Encoding: gzip themselves, and Nginx should generally not try to re-compress already-encoded content. If you see this, disable compression at the backend level and let Nginx handle it centrally, which is cleaner anyway.

High CPU usage after enabling gzip on a busy server. Lower gzip_comp_level from 6 down to 4 or so, or switch to gzip_static for assets that don’t change often, offloading compression to build time instead of request time.

Security Considerations

Gzip compression has one specific, well-documented security consideration worth knowing: the BREACH attack. This is a timing-based attack that can potentially leak secrets (like CSRF tokens) embedded in a compressed HTTPS response, by observing how compressed response sizes change based on guessed secret values.

This mostly matters for dynamic, authenticated pages where a secret token is reflected in the response body alongside user-controlled input. Mitigations include:

location /account/settings {
    gzip off;
    proxy_pass http://backend;
}

For the vast majority of static content — HTML pages, CSS, JS, public JSON — this isn’t a practical concern, and I don’t disable gzip broadly out of caution. It’s specifically an issue for secret-bearing, attacker-observable, compressible responses.

Performance Tips

Real-World Use Cases

Best Practices Summary

Enabling gzip properly is genuinely one of the best effort-to-payoff changes you can make to a web server. A handful of lines in nginx.conf, and every text-based response on the site gets meaningfully smaller for every visitor from that point forward.

Gzip vs. Brotli: Choosing an Approach

I get asked fairly often whether it’s worth adding Brotli on top of (or instead of) gzip. Here’s how I think about it in practice.

Brotli, developed by Google, generally achieves better compression ratios than gzip for text content — often 15-25% smaller output at comparable compression levels, which is a meaningful bandwidth saving at scale. The catch is that stock Nginx doesn’t ship with Brotli support built in; you need the ngx_brotli module, which either means compiling Nginx from source with the module included, or using a package/distribution that bundles it (some managed hosting providers and Docker images include it by default).

If you have Brotli available, I configure both, letting Nginx serve whichever the client supports, with Brotli preferred:

brotli on;
brotli_comp_level 5;
brotli_types text/plain text/css application/javascript application/json image/svg+xml;

gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;

Nginx automatically negotiates based on the client’s Accept-Encoding header — a browser that supports both will typically prefer Brotli, while older clients fall back to gzip. If compiling in Brotli support isn’t practical for your environment, gzip alone is still a very solid choice and remains universally supported without any extra module requirements.

Measuring the Real-World Impact

I don’t just take compression savings on faith — I measure it on real pages before and after, since the benefit varies a lot depending on what’s actually being served. A quick before/after comparison:

# Without compression
curl -s -o /dev/null -w "Uncompressed: %{size_download} bytes\n" https://example.com/app.js

# With compression
curl -s -H "Accept-Encoding: gzip" -o /dev/null -w "Compressed: %{size_download} bytes\n" https://example.com/app.js

For a typical minified JavaScript bundle, I routinely see reductions in the 65-75% range. JSON API responses compress even better in many cases — repetitive key names and structure across array elements are exactly the kind of pattern gzip handles well, sometimes hitting 80%+ reduction on larger payloads. Already-compressed binary content (images, video, most modern font formats like WOFF2, which is already compressed internally) typically shows little to no improvement, which is exactly why I exclude those formats from gzip_types rather than wasting CPU cycles compressing them anyway.

Frequently Asked Questions

Does gzip compression affect Time to First Byte (TTFB)? Slightly, since compression takes a small amount of CPU time before the response can be sent — but for the compression levels I recommend (4-6), this overhead is negligible compared to the transfer time saved, especially over slower connections. The tradeoff overwhelmingly favors compression except in extremely CPU-constrained environments.

Should I compress responses for internal API calls between backend services on the same network? Usually not worth it — compression exists to save network transfer time, and on a fast internal network (especially within the same data center or VPC), the CPU cost of compression may exceed the transfer time it saves. I typically reserve gzip for client-facing, internet-routed traffic.

Can gzip compression break anything? In modern setups, essentially no — gzip has been a web standard for decades and every mainstream browser and HTTP client handles it correctly. The main historical exception (very old Internet Explorer versions with buggy gzip support) is covered by the gzip_disable "msie6"; directive and is irrelevant for virtually all traffic today.

How do I know if gzip is already enabled by my hosting provider or a CDN in front of Nginx? Check response headers with curl -I -H "Accept-Encoding: gzip" at both the CDN edge and directly against your origin server (bypassing the CDN, if possible, using a direct IP or a --resolve override). If the CDN is already compressing on your behalf, you may not need to duplicate the work at the origin, though I still generally enable it at the origin anyway, since not every request necessarily passes through the CDN’s compression layer (cache misses, for example, still need origin compression to happen somewhere).

Exit mobile version