Decision records
ADR-0028: Open file cache for static files
- Status: Accepted (2026-10-06)
Context
Every static request performed a confined openat2, fstat, a read (small files) and
close. nginx offers open_file_cache for this; the benchmark compared both without it.
Decision
- Per document root, a bounded cache (
moka, default 10 000 entries, idle entries closed after 20 s) maps the normalised request path (and trailing-slash flag) to the opened handle, its metadata, the served path (index files resolved) and, for files up to 128 KiB, their content. - A hit is verified with one
statxof the served path: device, inode, size, mtime and ctime must equal the cached handle’s.statxis not confined, but it only decides whether the already-confined handle is still the file at that path; anything else (replaced, deleted, a symlink swapped in) is a miss and takes the confined lookup. server.open_file_cache.valid(default unset = verify every hit) skips verification for that long after the last one, like nginx’sopen_file_cache_valid(default 60 s there): changes then become visible after at mostvalid.- Requests that negotiate precompressed variants take the full path.
server.open_file_cache.max_entries: 0disables it.
Measurements (2026-10-06, same harness, both servers configured alike)
mode scalws small nginx small
off 72.2k 102.5k
verify every hit (valid 0) 79.4k 111.5k
trust 60 s 77.9k 149.3kThe cache gains ~10 % for scalws; skipping the remaining statx gains nothing more, so the
default verifies every hit. The rest of the gap is per-request overhead outside the file
system (ADR-0029).