Why the storage layer decides cache effectiveness
OPcache is usually treated purely as a PHP setting, yet its effectiveness depends directly on the underlying Linux filesystem. Inode behavior, mount options and network filesystems such as NFS can make the exact same OPcache configuration reliable on one server and unusable on another.
Table of Contents
- 1. Why OPcache is more than a PHP setting
- 2. validate_timestamps and the cost of stat()
- 3. Inode based invalidation and symlink deployments
- 4. NFS and other network filesystems
- 5. Moving opcache.file_cache onto tmpfs
- 6. Deployment strategies without cache breakage
- 7. Monitoring OPcache status per filesystem
- 8. Common mistakes with OPcache and filesystems
- 9. Filesystem options compared
- 10. Summary
- 11. FAQ
1. Why OPcache is more than a PHP setting
OPcache stores compiled PHP bytecode in shared memory so the Zend compiler does not need to parse every PHP file on each request. That description is accurate, but it hides the fact that OPcache constantly interacts with the Linux filesystem to decide whether a cached file is still current. This is exactly where most performance problems arise that get wrongly classified as PHP problems, when the actual root cause sits in the storage layer.
Every interaction with OPcache filesystem behavior costs time, and that time differs massively between a local NVMe mount, a network filesystem and a containerized overlay filesystem. On a Magento server with thousands of PHP files in the vendor directory, even small per file latencies add up to noticeable delays if the wrong combination of OPcache configuration and filesystem is chosen.
This article focuses specifically on the OPcache filesystem interactions that are usually missing from standard tutorials: the cost of validate_timestamps, the behavior with symlink based deployments, the pitfalls of NFS, and the option of placing the secondary file cache on faster storage on purpose.
2. validate_timestamps and the cost of stat()
With opcache.validate_timestamps = 1, PHP checks on every request whether a file has changed since the last caching operation. This check is performed via a stat() system call that reads the modification time from the filesystem's metadata. On a local ext4 or XFS filesystem with a warm page cache, this call only costs microseconds, but on a slow or overloaded storage backend this time can grow noticeably, especially given the several thousand files Magento potentially includes per request.
The check interval is additionally controlled by opcache.revalidate_freq, which specifies the minimum number of seconds that must pass between two timestamp checks for the same file. For production Magento servers, where deployments go through a controlled process rather than direct file edits in a running system, opcache.validate_timestamps = 0 is the significantly more performant choice. Every stat() call is then eliminated entirely, and a manual cache reset after deployment replaces the automatic check.
; /etc/php/8.3/fpm/conf.d/10-opcache.ini
; Production setting: skip stat() calls entirely on the filesystem
opcache.enable = 1
opcache.validate_timestamps = 0
opcache.revalidate_freq = 0
; Staging/dev setting: check timestamps every 2 seconds per file
; opcache.validate_timestamps = 1
; opcache.revalidate_freq = 2
3. Inode based invalidation and symlink deployments
OPcache identifies cached files not only by path but internally also by the inode number of the file within the OPcache filesystem context. This becomes relevant with deployment strategies that use atomic symlink swaps, a common pattern in Capistrano style deployments where every release directory receives a fresh physical copy of the code and a current symlink is repointed afterwards.
Since every new release copy creates new inodes, even when the file content stays identical, OPcache reliably detects the new version when validate_timestamps is active. With validate_timestamps disabled, however, which is exactly what is recommended for maximum performance, the OPcache memory must be actively flushed after every symlink swap, otherwise PHP-FPM keeps serving bytecode from the old release directory even though the symlink already points to the new path.
#!/usr/bin/env bash
# deploy-with-opcache-reset.sh — atomic symlink swap plus OPcache invalidation
set -euo pipefail
RELEASE_DIR="/var/www/magento/releases/$(date +%Y%m%d%H%M%S)"
CURRENT_LINK="/var/www/magento/current"
echo "[DEPLOY] Extracting new release to $RELEASE_DIR"
mkdir -p "$RELEASE_DIR"
tar -xzf /tmp/release.tar.gz -C "$RELEASE_DIR"
echo "[DEPLOY] Swapping symlink atomically"
ln -sfn "$RELEASE_DIR" "$CURRENT_LINK"
# Critical: new inodes are invisible to a warm OPcache with
# validate_timestamps=0 until workers are recycled or reset explicitly.
echo "[DEPLOY] Resetting OPcache via FPM reload"
systemctl reload php8.3-fpm
echo "[DEPLOY] Done — release live at $RELEASE_DIR"
4. NFS and other network filesystems
When the Magento code directory is shared over NFS between multiple application servers, additional OPcache filesystem pitfalls arise that do not exist on local storage. Each server maintains its own OPcache in local shared memory, with no knowledge of what the other servers hold in their caches. If a file changes on the NFS share, servers only see it once their own validate_timestamps check detects the change, which of course does not happen at all when the check is disabled.
A second, more subtle problem: NFS caches metadata client side for a certain duration, controlled via mount options such as actimeo. Even with opcache.validate_timestamps active, a stat() call on an NFS mount can return stale metadata, because the NFS client never actually contacts the server but instead serves its local metadata cache. For multi server setups with a shared code directory, a central, coordinated deployment process with an explicit OPcache reset on all nodes is therefore mandatory, a simple touch on a file is not reliably sufficient on NFS.
#!/usr/bin/env bash
# reset-opcache-cluster.sh — invalidate OPcache on all app servers after
# a shared NFS deployment, since local stat() caching hides changes
set -euo pipefail
declare -a APP_SERVERS=("app1.internal" "app2.internal" "app3.internal")
for host in "${APP_SERVERS[@]}"; do
echo "[RESET] Reloading PHP-FPM on $host"
ssh -o BatchMode=yes "deploy@${host}" "sudo systemctl reload php8.3-fpm"
done
echo "[RESET] OPcache invalidated on all ${#APP_SERVERS[@]} nodes"
5. Moving opcache.file_cache onto tmpfs
Besides the shared memory cache, OPcache offers a secondary, file based cache via opcache.file_cache that survives a PHP-FPM restart. Without this option, the entire bytecode must be recompiled after every restart, which on a large Magento codebase means several extra seconds for the first requests after a restart while the shared memory cache is still empty.
The storage location of this file cache is a direct OPcache filesystem decision. If opcache.file_cache sits on a slow disk, warm up after a restart slows down accordingly. A tmpfs mount, a RAM backed filesystem set up via /etc/fstab, delivers significantly faster read and write access for this secondary cache, with the tradeoff that its content is completely lost on a server reboot and must be rebuilt once.
; /etc/php/8.3/fpm/conf.d/10-opcache.ini
; Secondary file-based cache survives php-fpm restarts (not server reboots)
opcache.file_cache = /var/cache/opcache-file
opcache.file_cache_only = 0
opcache.file_cache_consistency_checks = 1
# /etc/fstab — mount opcache file_cache directory on tmpfs (RAM-backed)
tmpfs /var/cache/opcache-file tmpfs rw,size=512M,mode=0755,uid=www-data,gid=www-data 0 0
# Apply without reboot:
# mkdir -p /var/cache/opcache-file
# mount /var/cache/opcache-file
6. Deployment strategies without cache breakage
The most stable OPcache filesystem strategy for Magento combines three elements: validate_timestamps = 0 for maximum performance in production, a controlled deployment process with an explicit systemctl reload after every symlink swap, and a secondary file cache on fast storage for short warm up times after planned restarts. This combination avoids both the performance cost of constant stat() calls and the risk of serving stale bytecode.
For blue green deployments or canary releases, where two code versions exist simultaneously under different paths, it must additionally be ensured that each PHP-FPM pool is exclusively assigned to one code version. If multiple code versions share the same PHP-FPM pool, the shared memory cache mixes bytecode entries from different releases, which can lead to inconsistent behavior when classes or functions are defined differently between the versions.
7. Monitoring OPcache status per filesystem
The opcache_get_status() function returns, among other things, the number of cache hits, cache misses and the current memory utilization. A high ratio of cache misses to hits usually indicates that the OPcache memory is dimensioned too small, or that constant invalidations are happening due to OPcache filesystem behavior, for example a too short revalidate_freq combined with a slow storage backend.
A simple monitoring script that periodically queries opcache_get_status() via an internal diagnostic endpoint reveals whether opcache.memory_consumption is sized adequately. If the number of restarts due to a full cache increases regularly, either the allocated memory needs to be raised or opcache.max_accelerated_files needs to be adjusted to the actual number of files in the vendor directory.
8. Common mistakes with OPcache and filesystems
The most common mistake is leaving validate_timestamps at 1 in production out of concern that disabling the timestamp check might cause problems. In fact, this constant check on a high traffic Magento server with thousands of PHP files introduces measurable overhead without providing any benefit in a controlled deployment process, since files should not be edited directly in a running system anyway.
The second common mistake concerns multi server setups: after a deployment, only the most recently updated server gets reloaded, while other nodes in the cluster continue serving old bytecode from their local shared memory cache. Without a central deployment step that triggers systemctl reload on all application servers simultaneously, inconsistent responses arise depending on which server handles a request, an issue that is often only identified as an OPcache problem after a lengthy investigation.
9. Filesystem options compared
The choice of filesystem and mount options for the Magento codebase has a direct impact on OPcache filesystem performance, especially during warm up and invalidation.
| Setup | stat() latency | Multi server consistency | Recommendation |
|---|---|---|---|
| Local ext4/XFS | Very low | Independent per server, needs reload | Standard for single server setups |
| NFS share | High, metadata cache delays | Requires coordinated reset | Only with a central deploy script |
| tmpfs for file_cache | Minimal, RAM backed | Per server, content lost on reboot | For fast warm up after restarts |
| Overlay FS (container) | Variable, depends on storage driver | Isolated per container | Size memory_consumption generously |
For production Magento servers, the combination of local ext4 or XFS storage for the code, disabled timestamp checking and a tmpfs based secondary file cache is the most stable OPcache filesystem configuration. NFS setups are feasible, but they strictly require a coordinated deployment process that invalidates the cache synchronously across all involved nodes.
Mironsoft
Linux server tuning and performance optimization for Magento
Is your server serving stale code after every deployment?
We review your OPcache configuration together with the filesystem, set up coordinated cache invalidation for multi server setups, and optimize warm up time after restarts.
OPcache audit
Review and optimize validate_timestamps, memory_consumption and file_cache
Deployment automation
Set up coordinated cache invalidation across all application servers
Storage consulting
Find the right filesystem and mount strategy for your Magento stack
10. Summary
The OPcache filesystem interaction is reduced to a single setting in most tutorials, yet the underlying storage significantly co determines actual performance. Disabled timestamp checking saves every stat() call in production, but demands a controlled deployment process with an explicit cache reset in return. Symlink based deployments create new inodes that remain invisible without an active reset.
NFS setups add extra complexity through client side metadata caching, which can undermine even an active timestamp check. A secondary file cache on tmpfs significantly shortens warm up time after planned restarts. Anyone who understands these OPcache filesystem relationships avoids both unnecessary performance loss and the risk of serving stale code to customers.
OPcache Filesystem Considerations — The Essentials at a Glance
Timestamps
validate_timestamps = 0 in production, with an explicit reset after every deployment via reload.
Symlink deployments
New inodes remain invisible to the shared memory cache without an active reset.
NFS pitfalls
Client side metadata caching requires a coordinated reset across all application servers.
file_cache
Move it onto tmpfs for minimal warm up time after PHP-FPM restarts.