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
expiresvalue 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 and enable HTTP/2 and HTTP/3 support — together with try_files and expires this gives the main boost to site speed.