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 statx of the served path: device, inode, size, mtime and ctime must equal the cached handle’s. statx is 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’s open_file_cache_valid (default 60 s there): changes then become visible after at most valid.
  • Requests that negotiate precompressed variants take the full path.
  • server.open_file_cache.max_entries: 0 disables 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.3k

The 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).