Skip to content
DevOps devops webservers 6 min read

Gzip Compression & Caching

When a browser loads your website, it downloads files: HTML, CSS, JavaScript, images, and fonts. The smaller those files are, and the less often the browser has to re-download them, the faster your site feels. Nginx gives you two simple tools for this: gzip compression (shrinking text files before sending them) and caching (telling the browser to keep a copy so it does not ask again). This page shows you how to turn both on, why they matter, and the one mistake that quietly wastes your server’s CPU.

What gzip compression is

Gzip is a compression method built into every modern web browser. When you enable it, Nginx squeezes text-based files (HTML, CSS, JavaScript, JSON) before sending them over the network. The browser unzips them automatically on the other side. Text compresses extremely well, often by 70 to 90 percent, so a 300 KB JavaScript file might travel as a 60 KB file. The user downloads less, so the page loads faster, and you pay for less bandwidth (the amount of data your server sends).

The browser tells Nginx it can handle gzip by sending an Accept-Encoding: gzip header. Nginx only compresses when it sees this, so older clients still work fine.

When to use it: almost always, for text content. When NOT to use it: for files that are already compressed (JPEG, PNG, MP4, ZIP, woff2 fonts). Re-compressing them wastes CPU and can even make them slightly bigger.

Enabling gzip in Nginx

Open the main Nginx config file. On Ubuntu 22.04 and 24.04 LTS the gzip block lives in the http section of /etc/nginx/nginx.conf.

sudo nano /etc/nginx/nginx.conf

Inside the http { ... } block, add or update these lines:

# /etc/nginx/nginx.conf  (inside http { })

gzip on;
gzip_vary on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied any;
gzip_types
    text/plain
    text/css
    text/xml
    application/json
    application/javascript
    application/xml+rss
    image/svg+xml;

Here is what each line does:

DirectiveWhat it does
gzip onTurns compression on.
gzip_vary onAdds a Vary: Accept-Encoding header so proxies cache compressed and uncompressed versions separately.
gzip_comp_level 5Compression effort, 1 (fast) to 9 (smallest). Level 5 is the sweet spot; higher levels burn CPU for tiny gains.
gzip_min_length 256Skip files under 256 bytes. Tiny files are not worth compressing.
gzip_proxied anyCompress responses even when Nginx sits behind another proxy.
gzip_typesThe list of MIME types to compress. text/html is always compressed and does not need listing.

Notice the list contains only text formats. We deliberately leave out images, video, and fonts because they are already compressed.

Now test the config and reload Nginx so the change takes effect without dropping connections:

sudo nginx -t
sudo systemctl reload nginx

Output:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Verifying gzip works

Use curl to ask for a gzipped response and inspect the headers:

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

Output:

HTTP/2 200
content-type: text/css
content-encoding: gzip
vary: Accept-Encoding

The content-encoding: gzip line confirms Nginx compressed the file. If it is missing, the file type is probably not in your gzip_types list.

Caching static assets with expires headers

Compression shrinks the download. Caching avoids the download entirely. When you set a cache header on a file, the browser stores it locally and reuses it on the next visit instead of asking your server again. This is the single biggest win for repeat visitors.

Nginx sets this with the expires directive, which controls two response headers at once: the old Expires header and the modern Cache-Control header (the rule browsers actually follow). You apply it inside a location block that matches static file extensions.

Edit your site’s server block, typically in /etc/nginx/sites-available/:

sudo nano /etc/nginx/sites-available/example.com

Add a location block that matches static assets:

# /etc/nginx/sites-available/example.com  (inside server { })

location ~* \.(?:css|js|jpg|jpeg|png|gif|ico|svg|woff2)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
    access_log off;
}

What this does:

  • ~* makes the match case-insensitive, so .JPG and .jpg both match.
  • expires 30d tells browsers to keep these files for 30 days.
  • Cache-Control "public, immutable" allows shared caches to store the file and promises it will not change, so browsers skip even the “has this changed?” check.
  • access_log off stops logging every asset hit, keeping your logs clean and saving a little disk I/O.

Gotcha: long cache times are great until you ship a new version and users still see the old file. The standard fix is cache busting: include a content hash in the filename, like app.4f8a1c.js. Build tools (Vite, webpack) do this for you. Because the filename changes when the content changes, you can safely cache hashed assets for a year (expires 1y).

Reload Nginx and confirm:

sudo nginx -t && sudo systemctl reload nginx
curl -I https://example.com/app.4f8a1c.js

Output:

HTTP/2 200
cache-control: public, immutable
expires: Thu, 16 Jul 2026 10:00:00 GMT

Choosing cache durations

Different files need different lifetimes. Use this as a starting guide:

Asset typeSuggested expiresWhy
Hashed JS/CSS (app.abc123.js)1yFilename changes on every release, so caching forever is safe.
Images, fonts30d to 1yRarely change; long cache is fine.
Unhashed CSS/JS1h to 1dFilename stays the same, so cache short to allow updates.
HTML pagesno-cache or shortContent changes often; you want fresh pages.

Basic proxy caching

If Nginx sits in front of an application (acting as a reverse proxy, a server that forwards requests to your app and returns its response), you can also cache the app’s responses. This means Nginx answers repeat requests itself instead of bothering your slower backend every time.

First, define a cache storage area in the http block of /etc/nginx/nginx.conf:

# /etc/nginx/nginx.conf  (inside http { })

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m
                 max_size=500m inactive=60m;

This creates a 500 MB on-disk cache at /var/cache/nginx, with a 10 MB memory zone named app_cache to track keys. Entries unused for 60 minutes are dropped.

Then use it in a location block that proxies to your app:

location /api/ {
    proxy_pass http://127.0.0.1:3000;
    proxy_cache app_cache;
    proxy_cache_valid 200 10m;
    add_header X-Cache-Status $upstream_cache_status;
}

proxy_cache_valid 200 10m caches successful (200) responses for 10 minutes. The X-Cache-Status header reports HIT, MISS, or BYPASS so you can see caching working with curl -I.

Warning: never proxy-cache pages that depend on the logged-in user (account dashboards, carts). Nginx would serve one user’s cached page to another. Only cache public, identical-for-everyone responses, or exclude private routes from the cache.

Make sure the cache directory exists and reload:

sudo mkdir -p /var/cache/nginx
sudo nginx -t && sudo systemctl reload nginx

Best Practices

  • Compress text (HTML, CSS, JS, JSON, SVG) but never images, video, or woff2 fonts. They are already compressed.
  • Keep gzip_comp_level around 4 to 6. Level 9 doubles CPU cost for a few percent smaller files.
  • Cache hashed build assets for a year (expires 1y, immutable) and rely on filename hashing for updates.
  • Keep HTML uncached or short-lived so users always get the latest content.
  • Turn off access_log for static asset locations to reduce noise and disk writes.
  • Only proxy-cache public responses; exclude anything personalized to a logged-in user.
  • After every config change, run sudo nginx -t before reloading so a typo never takes your site down.
Last updated June 15, 2026
Was this helpful?