You ship a clean, fast website to your server, open it in a browser, and it still feels sluggish. The HTML is small, but the page pulls in CSS, JavaScript, fonts, and images, and every one of those files travels across the network in full size on every visit. Your server is healthy and your code is fine, but the bytes on the wire are working against you.
Two of the cheapest, highest-impact wins you can apply to any Nginx server are Gzip compression and browser caching. Compression shrinks text-based files before they leave the server, so they download faster. Browser caching tells the visitor’s browser to keep static files locally, so repeat visits skip the download entirely. Neither requires touching your application code, installing extra modules, or paying for anything. Both ship with the standard Nginx package on Ubuntu.
In this tutorial you will enable Gzip compression for the right file types, configure browser caching with sensible expiry times for different kinds of assets, and verify with real commands that both are actually working. This is aimed at developers, sysadmins, and DevOps engineers who run an Nginx server and want a measurable speed boost without a rewrite.
Conceptual Overview
Before editing config files, it helps to understand what these two features actually do, because they solve different problems.
Compression: smaller files on the wire
Gzip is a compression algorithm. Text-based files like HTML, CSS, JavaScript, JSON, and SVG contain a lot of repetition, which compresses extremely well, often by 70 to 90 percent. When a browser sends a request, it includes a header announcing what it can decompress:
Accept-Encoding: gzip, deflate, br
If Nginx sees gzip in that header and compression is enabled, it compresses the response on the fly, adds a Content-Encoding: gzip header, and sends the smaller version. The browser decompresses it transparently. The user sees the same page, just delivered with far fewer bytes.
One important detail: compression only helps text. Images (JPEG, PNG, WebP), video, and most archives are already compressed. Running Gzip on them wastes CPU and can even make them slightly larger, so we deliberately exclude those file types.
Browser caching: no download at all
Browser caching is about telling the browser, “this file will not change for a while, so keep your copy and do not ask me for it again.” Nginx does this by sending response headers like Cache-Control and Expires. On the next visit, the browser serves the file from its local disk instead of making a network request. The fastest request is the one that never happens.
The catch is cache invalidation. If you tell a browser to cache style.css for a year and then change the file, returning visitors keep the stale copy. The standard solution is to cache assets with versioned filenames (like style.abc123.css) aggressively, and cache HTML for a short time or not at all. We will configure exactly that pattern below.
These two features stack: compression makes the first download small, and caching removes the download entirely on repeat visits.
Prerequisites
- Ubuntu 20.04, 22.04, or 24.04
- Nginx installed and running. If you need it:
sudo apt install nginx - A non-root user with
sudoprivileges - A working site or server block. If you have not set one up yet, see Understanding Nginx Server Block
- Basic comfort editing files on the command line
Everything here uses modules built into the standard Nginx package, so there is nothing extra to compile or install.
Step 1: Check Your Starting Point
Before changing anything, measure what you have. This way you can prove the optimization worked.
Pick a CSS or JS file your site serves and request it with curl, asking for compression and printing only the response headers:
curl -s -I -H "Accept-Encoding: gzip" https://example.com/css/style.css
On a fresh Nginx install you will typically see something like this:
HTTP/2 200
server: nginx
date: Mon, 08 Jun 2026 09:00:00 GMT
content-type: text/css
content-length: 86230
last-modified: Sun, 07 Jun 2026 12:00:00 GMT
etag: "6683f...-150d6"
accept-ranges: bytes
Notice what is missing: there is no content-encoding: gzip line, so the file is sent uncompressed. And there is no cache-control line with a long max-age, so the browser will revalidate this file on every visit. Both of those are what we are about to fix.
Step 2: Enable Gzip Compression
Gzip settings belong in the http {} block so they apply to every site on the server. The standard place on Ubuntu is /etc/nginx/nginx.conf.
Open the file:
sudo nano /etc/nginx/nginx.conf
The default Ubuntu config already has a commented-out Gzip section inside http {}. Replace it (or add a new block) with this:
http {
# ... existing settings ...
# --- Gzip compression ---
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/json
application/xml
application/rss+xml
application/vnd.ms-fontobject
font/ttf
font/otf
image/svg+xml;
# ------------------------
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Here is what each directive does and why it is set this way:
gzip on;turns compression on. By itself it only compressestext/html, which is why the next directives matter.gzip_vary on;adds aVary: Accept-Encodingresponse header. This tells caches (including CDNs and proxies) to store the compressed and uncompressed versions separately, so a client that cannot handle Gzip is never served a compressed file it cannot read.gzip_proxied any;enables compression even for requests that come through a proxy. Without it, Nginx skips compression for proxied requests, which matters if Nginx sits behind a load balancer or CDN.gzip_comp_level 5;sets the compression effort from 1 (fastest, least compression) to 9 (slowest, most compression). Level 5 is the sweet spot. Going from 5 to 9 usually shaves only a few percent off the file size while burning noticeably more CPU per request.gzip_min_length 256;skips compression for tiny files. Compressing a 40-byte response can produce something larger than the original because of Gzip’s overhead, so we only bother with files of at least 256 bytes.gzip_typeslists the MIME types to compress.text/htmlis always compressed and does not need to be listed. We add the common text-based assets: CSS, JavaScript, JSON, XML, SVG, and web font formats. We deliberately leave out JPEG, PNG, WebP, and video because they are already compressed.
Save and close the file.
Step 3: Configure Browser Caching
Caching rules are tied to specific paths, so they belong in your site’s server block, usually in /etc/nginx/sites-available/. Open your site config:
sudo nano /etc/nginx/sites-available/example.com
Add location blocks that set caching headers based on file type:
server {
listen 443 ssl;
server_name example.com;
root /var/www/example.com;
index index.html;
# Versioned, fingerprinted assets: cache hard for a year
location ~* \.(?:css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Images and fonts: cache for a month
location ~* \.(?:jpg|jpeg|png|gif|webp|svg|ico|woff|woff2|ttf|otf)$ {
expires 30d;
add_header Cache-Control "public";
access_log off;
}
# HTML: revalidate every time, never serve stale markup
location ~* \.(?:html)$ {
expires -1;
add_header Cache-Control "no-cache";
}
location / {
try_files $uri $uri/ /index.html;
}
}
Let’s walk through the logic:
- The
~*makes the location match a case-insensitive regular expression on the request path, so.CSSand.cssboth match. expires 1y;is an Nginx shortcut that sets both theExpiresheader andCache-Control: max-age=31536000. For CSS and JS, this assumes you use versioned filenames (for exampleapp.9f2b1c.js) produced by a build tool. Because the filename changes whenever the content changes, caching the old name forever is safe.add_header Cache-Control "public, immutable";reinforces the long cache.publicallows shared caches to store it, andimmutabletells modern browsers not to even revalidate the file during its lifetime, which removes a class of unnecessary conditional requests.- For images and fonts, 30 days is a reasonable balance. These usually do not have versioned names, so we avoid caching them for a full year.
- For HTML,
expires -1;plusCache-Control: no-cachemeans the browser must check with the server before using its cached copy. This guarantees visitors always get the latest markup, which in turn points to the latest versioned CSS and JS. This is the key to safe cache invalidation: cache the assets forever, but never cache the HTML that references them. access_log off;on static assets keeps your access log focused on meaningful requests instead of every font and icon. This is optional but keeps logs readable.
If you have not added SSL to your site yet, that is the natural companion to this setup. See How to Secure Nginx with Let’s Encrypt SSL Using Certbot on Ubuntu.
Save and close the file.
Step 4: Test and Reload Nginx
Never reload without testing first. A single typo can take your site offline:
sudo nginx -t
Expected output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
If the test passes, reload Nginx so it picks up the changes without dropping active connections:
sudo systemctl reload nginx
Step 5: Verify Compression Works
Run the same curl command from Step 1 again:
curl -s -I -H "Accept-Encoding: gzip" https://example.com/css/style.css
This time you should see the compression and caching headers:
HTTP/2 200
server: nginx
content-type: text/css
last-modified: Sun, 07 Jun 2026 12:00:00 GMT
etag: "6683f...-150d6"
content-encoding: gzip
vary: Accept-Encoding
cache-control: public, immutable
expires: Tue, 08 Jun 2027 09:00:00 GMT
The important lines are content-encoding: gzip (compression is on), vary: Accept-Encoding (caches store both versions), and cache-control plus expires (browser caching is on).
To actually see the byte savings, compare the compressed and uncompressed sizes:
# Uncompressed size
curl -s -o /dev/null -w "uncompressed: %{size_download} bytes\n" \
https://example.com/css/style.css
# Compressed size
curl -s -o /dev/null -H "Accept-Encoding: gzip" \
-w "compressed: %{size_download} bytes\n" \
https://example.com/css/style.css
Sample output:
uncompressed: 86230 bytes
compressed: 14905 bytes
That is roughly an 83 percent reduction, and it costs you nothing but a few lines of config.
Step 6: Verify Browser Caching in the Browser
Headers prove the server is sending the right instructions, but you can also confirm the browser obeys them.
Open your site in Chrome or Firefox, press F12 to open Developer Tools, and go to the Network tab. Reload the page once, then reload it again. On the second load, look at the Size column for your CSS, JS, and image files. Instead of a byte count you should see (memory cache) or (disk cache), which means the browser served those files locally without contacting the server.
This is the win that matters most for real users: on repeat visits, the heavy static assets do not travel the network at all.
Common Mistakes and Troubleshooting
content-encoding: gzip does not appear.
The most common cause is a missing MIME type. Nginx only compresses the types listed in gzip_types (plus text/html). If your JavaScript is served as application/javascript but you only listed text/javascript, it will not be compressed. Confirm the file’s content type first:
curl -s -I https://example.com/js/app.js | grep -i content-type
Then make sure that exact type is in your gzip_types list.
Files are still not compressed even though the type is listed.
Check that the file is bigger than gzip_min_length. A 100-byte file with a 256-byte minimum will never be compressed. Also remember that if Nginx is proxying to a backend, you need gzip_proxied any; or compression is skipped for those responses.
The browser keeps showing an old version of a file.
This is cache invalidation biting you. If you set expires 1y on a file with a fixed name like style.css and then edited it, returning visitors keep the cached copy until it expires. The fix is to use versioned filenames from your build tool, or during development, hard-reload with Ctrl+Shift+R to bypass the cache. In production, never apply a one-year cache to files whose names do not change.
add_header directives seem to disappear.
Nginx add_header directives are not inherited into a child block if that child block defines its own add_header. If a location adds its own header, it replaces the entire set from the parent context rather than merging. If you rely on headers from an outer block, repeat them in the inner block, or keep all related add_header directives in the same context.
nginx -t fails after editing.
Read the error message; it names the file and line number. A frequent culprit is a missing semicolon at the end of a directive, or placing an http-level directive like gzip_types inside a server block by mistake. Compression directives go in http {}; caching location blocks go in server {}.
Best Practices
Do not compress already-compressed files. Keep JPEG, PNG, WebP, MP4, and ZIP out of gzip_types. Compressing them wastes CPU for zero or negative gain. Compress text, serve binaries as-is.
Match cache lifetime to how the file is named. Versioned, fingerprinted assets (the filename changes when the content changes) can be cached for a year with immutable. Files with stable names should get a shorter lifetime so updates reach users in a reasonable window. HTML should almost always be no-cache so it can point browsers to the newest assets.
Pre-compress large static files for even more savings. For files that rarely change, the ngx_http_gzip_static_module (included in the standard package) lets Nginx serve a pre-built .gz file instead of compressing on every request. Generate style.css.gz alongside style.css, add gzip_static on;, and Nginx serves the pre-compressed version with no per-request CPU cost. This pairs well with high gzip_comp_level builds since the cost is paid once.
Combine caching headers with server-side caching. Browser caching helps repeat visitors. If you also serve many first-time visitors or proxy to a slow origin, add server-side caching too. For that pattern, see How To Configure NGINX as CDN, which uses proxy_cache to cache responses on the server itself.
Protect the performance you gained. Compression and caching make legitimate traffic cheaper, but they will not stop abuse. Pair them with request throttling so a single client cannot overwhelm the server. See How to Configure Rate Limiting in Nginx on Ubuntu.
Re-test after every config change. Make curl -I -H "Accept-Encoding: gzip" part of your deployment checklist. It is a five-second check that catches a surprising number of regressions, like a new file type that nobody added to gzip_types.
Conclusion
You enabled Gzip compression in the http {} block for the right text-based file types, configured browser caching with a layered strategy (cache versioned assets for a year, images and fonts for a month, and HTML not at all), and verified both with curl and the browser’s Network tab. The result is smaller downloads on the first visit and almost no downloads on repeat visits, all without changing a single line of application code.
These two optimizations are among the best return-on-effort changes you can make to any Nginx server. They are quick to apply, safe when configured correctly, and they benefit every visitor on every page.
From here, good next steps are:
- Add a CDN-style server cache in front of a slow backend, see How To Configure NGINX as CDN
- Distribute traffic across multiple backends so performance holds under load, see Configure Nginx as a Layer 7 Load Balancer
- Load test your tuned server to measure the real-world improvement, see Load Testing with k6 on Ubuntu