Linux VPS

Serve static files fast with Nginx: sendfile, open_file_cache and caching headers

Tune how Nginx serves images, CSS, JS and downloads: sendfile, tcp_nopush, open_file_cache, expires and Cache-Control, precompressed files, large downloads and avoiding needless logging.

IntermediateLinux VPS3 min read

Images, stylesheets, scripts and fonts often make up most requests on a page. Nginx is very good at serving static files, but a few directives make it even faster and, more importantly, let browsers and CDNs avoid asking again. This guide covers the kernel-level optimisations and the HTTP caching headers that matter.

Before you start

  • Nginx on a VPS with sudo access.
  • Asset filenames that change when content changes (e.g. app.3f9a1c.js), which most build tools and WordPress plugins provide. This makes long caching safe.

sendfile, tcp_nopush and tcp_nodelay

In the http context:

sendfile on;
tcp_nopush on;
tcp_nodelay on;
  • sendfile lets the kernel copy file data straight to the network socket, without passing through Nginx's memory. Less CPU, fewer copies.
  • tcp_nopush (with sendfile) sends response headers and the start of the file in full packets.
  • tcp_nodelay sends the last small packets immediately, so keep-alive responses don't wait.

Most distribution configs already enable these. With HTTPS, sendfile only helps if the kernel supports TLS offload (kTLS, ssl_conf_command Options KTLS; with recent OpenSSL and Nginx); otherwise Nginx encrypts in user space anyway, which is still fast.

open_file_cache

Every request for a file means opening it and checking its metadata. Nginx can cache file descriptors and lookups:

open_file_cache max=10000 inactive=60s;
open_file_cache_valid 120s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
  • max: number of entries.
  • inactive: drop entries not used in this time.
  • valid: how often to re-check that a cached entry is still current. A deployed file may be served from the old handle for up to this long.
  • errors on: also cache "file not found", useful against floods of missing-file requests.

This helps most on sites with many small files and high request rates.

Long browser caching for versioned assets

location ~* \.(?:css|js|mjs|woff2?|ttf|otf|svg|jpe?g|png|gif|webp|avif|ico)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable" always;
    access_log off;
    try_files $uri =404;
}
  • immutable tells browsers never to revalidate during the max-age; perfect for fingerprinted filenames.
  • For files whose names don't change (e.g. logo.png referenced everywhere), use a shorter max-age such as a week.
  • Remember that add_header here replaces server-level headers in this location; re-include your security headers snippet. See security headers.

HTML: short or no caching

location / {
    add_header Cache-Control "no-cache" always;
    try_files $uri $uri/ /index.html;
}

no-cache doesn't mean "don't store"; it means "revalidate first". Nginx answers revalidation with 304 Not Modified using ETag and Last-Modified, which it sends automatically for static files. More on headers: Cache-Control headers and CDNs.

Serve precompressed files

gzip_static on;
brotli_static on;

If app.js.gz or app.js.br exists next to app.js, Nginx sends it directly instead of compressing on every request. Generate them at maximum compression in your build. See web server compression. (brotli_static needs the Brotli module.)

Large downloads and video

  • Nginx supports byte-range requests automatically, so video seeking and resumed downloads work.
  • For big files on fast networks, sendfile_max_chunk 2m; stops one fast client from hogging a worker.
  • Throttle per connection if needed: limit_rate_after 10m; limit_rate 2m;.
  • For MP4 streaming with seeking by time, the mp4 module (location ~ \.mp4$ { mp4; }) helps.

Verify

curl -sI https://example.com/assets/app.3f9a1c.js | grep -iE 'cache-control|expires|etag|content-encoding'
curl -sI -H 'If-None-Match: "PASTE-ETAG"' https://example.com/index.html | head -1

The second command should return 304.

Troubleshooting

  • Old CSS after deploy: long caching on non-versioned filenames. Version the files or shorten max-age.
  • Security headers missing on assets: location-level add_header replaced them.
  • Files 404 after moving them: open_file_cache still holds the old lookup until valid expires; reload Nginx.

FAQ: Nginx static files

Do I still need a CDN?

A CDN serves files from locations near visitors and offloads bandwidth; Nginx tuning makes the origin efficient. They complement each other.

Is open_file_cache safe?

Yes, with a short valid period.

Should static files be logged?

Usually not; turning logs off for them saves disk I/O.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.