Skip to content

[6.x] Speed up reads from the file cache store - #15837

Closed
ryanmitchell wants to merge 1 commit into
statamic:6.xfrom
ryanmitchell:stache-filestore-read
Closed

ryanmitchell wants to merge 1 commit into
statamic:6.xfrom
ryanmitchell:stache-filestore-read

Conversation

@ryanmitchell

Copy link
Copy Markdown
Contributor

Statamic replaces the file cache driver with its own Statamic\Extensions\FileStore, which the Stache uses on the default setup. Reads go through Laravel's FileStore::get(), which:

  • checks the file exists with is_file() before opening it, and
  • calls Carbon::now() twice per read to check expiry, even for items stored forever.

The Stache reads far more often than it writes, and almost everything it stores never expires. This PR overrides get() to open the file directly (a failed open is a cache miss) and only read the clock for items that can actually expire.

Everything else matches Laravel's behaviour:

  • It takes the same shared lock, so a read never sees a file mid-write.
  • Expired, empty and corrupt files are forgotten.
  • serializable_classes is respected on Laravel versions that support it.

It applies to all keys in the store, not just Stache keys, since the behaviour is the same as Laravel's.

Benchmark

Before After
Warm read of 2,000 Bard-heavy entries through the Stache (macOS), fastest of 7 ~230ms ~213ms (7–11% faster)
Same, median ~245ms ~217ms
Reading 20,000 small cache items in isolation (Linux, ext4, PHP 8.4) 422ms 306ms (25–28% faster)

The saving is a fixed overhead of roughly 10µs per read. It's a smaller share when unserializing large entries dominates, and a bigger one for requests that read many items from the Stache.

Tests

The existing FileStoreTest now also covers reading values (including false, null and missing keys, and many()), expiry, corrupt files and allowed classes, each for both stache:: keys and other keys.

🤖 Generated with Claude Code

Laravel's FileStore::get() checks the file exists before reading it and
reads the clock twice, even for items stored forever. The Stache reads
far more often than it writes and almost never stores items that
expire, so open the file directly and only check the clock for items
that can expire. Keep the shared lock, and forget expired or corrupt
files as Laravel does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jasonvarga

Copy link
Copy Markdown
Member

Thanks Ryan! I've decided not to merge this into Statamic. I had a few different agents benchmark it locally, and they found a smaller saving than the PR describes: about 2–4µs per cache hit on macOS rather than ~10µs. A typical page does a few hundred Stache reads, so that comes out to well under a millisecond per request, and sites using static caching won't see any difference. Cache misses actually got a bit slower (~1.75µs → ~3.1µs), because the suppressed fopen warning still goes through Laravel's error handler. The override applies to every file-cache key, not just Stache ones.

Weighed against that, we'd be maintaining our own copy of Laravel's read path across 12.x and 13.x. Future fixes to getPayload() or Filesystem::get() would no longer reach us, and there are some small differences in behaviour: Throwable vs Exception handling, skipping the isFile() guard, and increment()/touch() still going through the parent.

Skipping the redundant is_file() check and the extra Carbon::now() calls is a nice idea, though. It isn't Statamic-specific, so it feels like a better fit as a PR to laravel/framework, where everyone using the file cache would benefit and we wouldn't need an override. Your tests here would carry over well.

@jasonvarga jasonvarga closed this Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants