# Serving Static Files in Nginx: try_files and expires

> How to configure fast static file delivery in Nginx: try_files, expires, sendfile, tcp_nopush, and browser caching directives.

## Why static files are better served directly

Images, CSS, JS, and fonts do not require running PHP code, so passing them through PHP-FPM is a waste of resources. Nginx can serve files straight from disk, many times faster and with less load on the server. The administrator's job is to tell Nginx exactly which paths to serve directly and which to pass to the backend.

## How try\_files works

The `try_files` directive checks files and directories in order and serves the first one found, and if nothing is found it passes the request further:

```
location / {
    root /var/www/example.com/public;
    try_files $uri $uri/ /index.php?$query_string;
}
```

Here Nginx first looks for a file with the exact request name, then a directory with that name, and only if nothing was found does it pass the request to `index.php`. This is the standard scheme for WordPress, Laravel, and most modern PHP applications.

## How to serve static files in a separate location

For static files it makes sense to set up a separate block with its own caching settings:

```
location ~* \.(jpg|jpeg|png|gif|webp|svg|css|js|woff2?)$ {
    root /var/www/example.com/public;
    try_files $uri =404;
    expires 30d;
    add_header Cache-Control "public, immutable";
    access_log off;
}
```

The `access_log off` setting for static files reduces disk load: there is no point logging thousands of requests for images and scripts.

## Caching and performance directives

| Directive | Value | Effect |
| --- | --- | --- |
| expires | 30d, 1y | The Cache-Control and Expires header for the browser |
| sendfile | on | Serving a file via the sendfile system call, without copying it into Nginx memory |
| tcp\_nopush | on | Sending headers and the start of a file in a single TCP packet |
| open\_file\_cache | max=1000 inactive=20s | A cache of file descriptors and metadata, to avoid reading the disk again |

The `sendfile` and `tcp_nopush` directives are enabled once in the `http {}` section — they work globally and need no per-site configuration.

## How to configure open\_file\_cache

On sites with a large number of small files — icons, avatars, thumbnails — it is useful to enable a descriptor cache:

```
open_file_cache max=2000 inactive=30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
```

This reduces the number of `open()` and `stat()` system calls for files that are requested often: Nginx keeps their descriptors in memory instead of reading the disk again.

## Common mistakes when serving static files

What most often breaks static file delivery:

- too long an `expires` value for files without a version in the name — an updated CSS file will not reach the user
- a regular expression in location without escaping the dot — it will match extra paths
- missing `try_files $uri =404;` — a request for a non-existent file goes to PHP-FPM for nothing
- root specified in two different places in the configuration with different paths — a typical cause of 404 on static files

## Summary

A checklist for fast static file delivery:

- move static extensions into a separate location with try\_files $uri =404;
- set expires to 30 days or more and Cache-Control: immutable for files with a version in the name
- enable sendfile, tcp\_nopush, and open\_file\_cache in http {}
- disable access\_log for static files to avoid loading the disk

After configuring static files, check [response compression with gzip and Brotli](https://www.zevshost.net/en/kb/nginx-gzip-brotli/) and enable [HTTP/2 and HTTP/3 support](https://www.zevshost.net/en/kb/nginx-http2-http3-quic/) — together with try\_files and expires this gives the main boost to site speed.

## Frequently asked questions

### If access\_log is turned off for static files, does that mean errors on those files stop being logged too?

No, access\_log off only stops successful and routine requests from being written to the log; error logging for that location keeps working independently, since it is controlled by a separate error\_log directive. Turning off access logging just reduces disk writes for the flood of ordinary image, script, and style requests, not your ability to see genuine failures.

### If a CSS file gets updated while expires is set to a long duration, how do you make sure visitors actually receive the new version?

Changing the expires value alone will not help: browsers that already cached the file keep using their copy until it expires. The fix is giving the file a different name or a version marker, usually during the build process, so the changed file is requested at a new address instead of waiting for the old cached copy to expire.

### Can the same file extension be served from more than one location block, or does Nginx report a conflict between them?

Nginx does not report a conflict on its own; it picks one matching location block by its own rules, usually favoring the most specific match. Overlapping blocks for the same extension class are more often a configuration mistake than a deliberate choice, and they tend to produce confusing behavior such as mismatched caching headers on requests that otherwise look identical.

### Is it safe to set an extremely long expires value, essentially forever, for every static file on a site?

That only makes sense for files whose name changes with their content, such as ones carrying a version marker or a hash — there an effectively permanent cache is the whole point. For a plain filename that gets overwritten in place, a very long cache turns into stale content stuck in visitors' browsers long after the server has been updated.

---

Source: https://www.zevshost.net/en/kb/nginx-otdacha-statiki/
