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.confor 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 aVary: Accept-Encodingresponse 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).anymeans 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 compressestext/htmleven withgzip 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_staticover dynamic compression for assets that don’t change per-request — it eliminates CPU overhead on every single request. - Tune
gzip_comp_levelbased 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_typesentirely, 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_brotlimodule) 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/jsonexplicitly added togzip_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_staticas 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_levelfrom 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_typesbeyond 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_staticfor 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).