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

  • Nginx installed and running.
  • Sudo access to edit nginx.conf or a config included from it.
  • A site already serving some HTML/CSS/JS to test against.

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:

  • gzip on; — the master switch.
  • gzip_vary on; — adds a Vary: Accept-Encoding response header, which tells caches (browsers, CDNs, proxies) that the response varies depending on whether the client supports compression. Without this, a caching proxy might serve a gzip response to a client that can’t decompress it, or vice versa.
  • gzip_proxied any; — controls whether Nginx compresses responses for proxied requests (i.e., when Nginx is acting as a reverse proxy in front of a backend). any means always compress, which is what I use unless I have a specific reason not to.
  • gzip_comp_level 6; — compression level from 1 (fastest, least compression) to 9 (slowest, most compression). Level 6 is the sweet spot I use almost everywhere — noticeably smaller output than level 1-3, without the CPU cost of level 9.
  • gzip_buffers 16 8k; — sets the number and size of buffers used for compression.
  • gzip_http_version 1.1; — only compress for HTTP/1.1+ clients (essentially all clients today; HTTP/1.0 had inconsistent gzip support).
  • gzip_types — the actual list of MIME types to compress. This is the part people forget to expand. By default, Nginx only compresses text/html even with gzip on; — everything else needs to be explicitly listed.

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";
  • gzip_min_length 256; — don’t bother compressing responses smaller than 256 bytes. Compression has overhead (the gzip header/footer itself, plus CPU time), so for tiny responses, compressing can actually make the payload larger or waste CPU cycles for negligible savings.
  • gzip_disable "msie6"; — a legacy compatibility setting for very old Internet Explorer versions that had broken gzip support. Realistically irrelevant for almost everyone today, but harmless to include and it’s still the standard recommendation in Nginx’s own documentation.

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:

  • Not reflecting secrets in compressible response bodies at all (use headers or cookies instead of embedding tokens in HTML where avoidable).
  • Adding random padding to responses containing sensitive tokens (some frameworks do this automatically).
  • Disabling compression specifically for the small subset of sensitive, dynamic endpoints where this pattern applies, while keeping it enabled everywhere else:
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

  • Prefer gzip_static over dynamic compression for assets that don’t change per-request — it eliminates CPU overhead on every single request.
  • Tune gzip_comp_level based on your CPU headroom. Level 6 is a good default; drop to 4 if your server is CPU-constrained and traffic is high.
  • Don’t bother compressing already-compressed formats (images, videos, zip files) — leave those out of gzip_types entirely, since compressing them wastes CPU for little to no size benefit and can occasionally make files slightly larger.
  • Pair gzip with browser caching. Compression reduces transfer size on each request; caching eliminates the request entirely on repeat visits. They solve different problems and work well together.
  • Consider Brotli as well. Brotli (ngx_brotli module) typically compresses text content even more effectively than gzip, especially at higher compression levels, though it requires a separate module not compiled into stock Nginx by default. Many people configure both, with Brotli as the preferred encoding and gzip as a fallback for older clients.

Real-World Use Cases

  • A JSON-heavy API where average response payloads dropped from roughly 40KB to under 10KB after enabling gzip with application/json explicitly added to gzip_types — a mistake I see often, since people remember CSS/JS but forget JSON.
  • A marketing site with large CSS bundles from a UI framework, where pre-compressing with gzip_static as part of the deploy pipeline eliminated compression CPU cost entirely for the highest-traffic assets.
  • A CPU-constrained low-cost VPS, where I dropped gzip_comp_level from 6 to 3 after noticing compression was contributing measurably to load average during traffic spikes — a good reminder that “more compression” isn’t free.

Best Practices Summary

  • Always expand gzip_types beyond the default — HTML alone isn’t enough.
  • Set gzip_vary on; so caches don’t serve the wrong version to the wrong client.
  • Use gzip_proxied any; if Nginx sits in front of a backend application.
  • Skip compressing binary formats that are already compressed.
  • Use gzip_static for assets that don’t change often, especially as part of a build pipeline.
  • Be aware of BREACH-style risks for dynamic, secret-bearing responses, and disable compression selectively there if relevant.
  • Test with curl -H "Accept-Encoding: gzip" after every change — don’t assume it’s working just because the config parsed cleanly.

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).

Total
1
Shares

Leave a Reply

Previous Post
How to Set Up Nginx with PHP-FPM

How to Set Up Nginx with PHP-FPM

Next Post
How to Create Custom Error Pages in Nginx

How to Create Custom Error Pages in Nginx

Related Posts