🗄️
Hosting

Caching static files in Nginx: Cache-Control, expires and versioning

07.11.2026
← All articles

Most of a web page is made of files that don't change between visits: logos, fonts, CSS, JavaScript libraries, product photos. If the server never tells the browser how long to keep them, the browser re-requests them or at least asks whether they changed. With correct caching headers, a returning visitor loads those files straight from disk without touching the network, and the page opens noticeably faster.

How the browser cache works

The Cache-Control header says how long a file stays fresh; max-age=2592000 means 30 days, during which the browser won't ask the server at all. After that it sends a conditional request with the old ETag in If-None-Match or a date in If-Modified-Since, and an unchanged file gets a tiny 304 Not Modified reply. The older Expires header sets an absolute date; when both exist, Cache-Control wins. Nginx's expires directive conveniently sets both.

The catch: updating files

Long caching means a changed CSS file may keep showing its old copy until the cache expires. The standard fix is versioned filenames: build tools like Vite or Webpack add a content hash, such as app.3f9a2c.css. A new file gets a new name, HTML points to it, and the browser fetches it. Such files can be cached for a year as immutable. Without a build tool, a query string like style.css?v=12 works, and WordPress adds version parameters automatically. Never cache the HTML itself for long, or visitors won't see the new filenames.

Nginx configuration

# Versioned CSS and JS: one year, immutable
location ~* \.(?:css|js)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
}

# Images and fonts: 30 days
location ~* \.(?:png|jpe?g|gif|webp|avif|svg|ico|woff2?)$ {
    expires 30d;
    add_header Cache-Control "public";
    access_log off;
}

# HTML: revalidate every time
location ~* \.html$ {
    expires -1;
}

Turning off access logs for static files cuts disk writes and keeps logs readable.

The add_header trap

If a location block contains any add_header, it inherits none of the add_header directives from the server or http level. Security headers like Strict-Transport-Security set at server level will silently vanish from image and CSS responses. Repeat them in each location or move them into a file pulled in with include.

Server-side: open_file_cache

Nginx can also keep metadata about frequently served files (descriptors, size, modification time) in memory, reducing disk lookups:

open_file_cache max=10000 inactive=60s;
open_file_cache_valid 120s;
open_file_cache_min_uses 2;
open_file_cache_errors on;

This caches metadata, not file contents; the OS page cache and sendfile on; handle fast delivery of the data itself.

Example: a private clinic in Namangan

A Namangan clinic's site has doctor photos, a services list, a booking form and a map, and patients come back often to check hours, prices or the address. With 30-day caching for images and a year for versioned CSS and JavaScript, a repeat visit fetches only the HTML from the server. When the admin updates the styles, bumping the version number makes the new design appear immediately.

On shared hosting: .htaccess

<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType text/css "access plus 1 year"
  ExpiresByType application/javascript "access plus 1 year"
  ExpiresByType image/webp "access plus 30 days"
  ExpiresByType image/jpeg "access plus 30 days"
  ExpiresByType image/png "access plus 30 days"
  ExpiresByType font/woff2 "access plus 30 days"
  ExpiresByType text/html "access plus 0 seconds"
</IfModule>

On many shared servers Nginx serves static files directly and Apache only handles PHP, so these rules may not affect static files at all. Always verify, and ask sayt.uz support how caching is configured on your plan if needed.

Verifying

curl -sI https://example.uz/assets/app.3f9a2c.css | grep -iE "cache-control|expires|etag"
curl -sI https://example.uz/ | grep -i cache-control

The first should show a long max-age, the second a short or zero one for HTML. In the browser's Network tab, cached files show as "disk cache" or "memory cache". If you use a CDN such as Cloudflare, remember it applies its own caching rules and may rewrite headers.

Related articles

💽 Disk full: 500 errors, a broken .htaccess and repairing the database 🍃 MongoDB: Atlas cloud or your own server — which to choose 🐘 Creating a PostgreSQL database on hosting and connecting your site 📦 Backup plugins fill up your disk: configuring UpdraftPlus and others properly
🌐 Language
🇺🇿 O'zbek 🇺🇿 Ўзбек 🇷🇺 Русский 🇬🇧 English ✓