OPcache for Performance: The Essentials for Tuning

Know the small set of OPcache settings and behaviors that actually affect throughput and latency in production, without re-deriving how opcode caching works internally.

This lesson assumes you already understand what OPcache is and how it works in depth — that full explanation, including how PHP compiles source into opcodes, how the shared memory segment is structured, how the JIT fits in, and how invalidation and preloading operate under the hood, lives in the OPcache and JIT lesson. Here, the lens is purely performance: which handful of settings matter when you're trying to reduce latency or increase throughput, and which common misconfigurations quietly cost you the very benefit OPcache is supposed to provide.

The performance case for OPcache in one paragraph

Without OPcache, PHP re-parses and re-compiles every script on every single request — pure CPU work that produces the identical result each time for unchanged files. OPcache removes that redundant compilation by caching the compiled opcodes in shared memory, so this cost is paid once per file (until it changes) instead of once per request. For a busy application, this alone routinely cuts response time and CPU usage significantly, since compilation is not a trivial fraction of a typical request's PHP-side work. For the mechanics of shared memory and how opcodes are structured, see the OPcache and JIT lesson.

What to actually tune for production throughput

Most performance problems with OPcache are not about the JIT or exotic settings — they're about the cache running out of room or being invalidated more than it needs to be. Three settings account for nearly all real-world tuning work:

• opcache.memory_consumption: the shared memory pool size in MB. If it's too small for your codebase, OPcache evicts and recompiles files, silently degrading back toward uncached performance. Check opcache_get_status()['memory_usage'] and opcache_get_status()['opcache_statistics']['opcache_hit_rate'] in production — a hit rate below ~99% or frequent 'wasted memory' warnings means this needs to go up. • opcache.max_accelerated_files: must exceed the actual number of PHP files your application loads (including vendor/ dependencies), or OPcache starts evicting entries on a rotating basis well before memory is the constraint. Undercounting this is one of the most common silent performance bugs in Composer-heavy applications. • opcache.validate_timestamps and opcache.revalidate_freq: in production, the standard practice is validate_timestamps=0, which stops OPcache from checking the filesystem for changes on every request — removing a stat() call per file per request. The tradeoff is that a deploy will not be picked up automatically; deployment tooling must call opcache_reset() (or restart PHP-FPM) as part of the release process. Leaving validate_timestamps on in production reintroduces filesystem I/O that OPcache exists to remove.

Preloading, in short

opcache.preload loads a specified set of files into the opcode cache once, at PHP-FPM startup, ahead of any request — those preloaded classes and functions are then available to every worker without the normal 'compile on first request per worker' cost. It can meaningfully reduce the CPU cost of the first requests each worker serves, but it has real operational sharp edges: preloaded code is frozen for the lifetime of the master process, changing it requires a full restart (not just a reset), and subtle bugs can appear if preloaded classes hold state shared across otherwise-isolated requests. Treat preloading as an advanced, deploy-pipeline-aware optimization rather than a default — the full tradeoffs and setup are covered in the OPcache and JIT lesson.

Code Examples

This is the quickest production health check for OPcache: a low hit rate or high wasted memory means the cache is thrashing (usually memory_consumption or max_accelerated_files is too small), and a cached-file count close to the configured max means you're about to start silently evicting entries under normal growth. Expected result: Hit rate: 99.87% Free memory: 42.3 MB Wasted memory: 1.1 MB Cached files: 6412 / max_accelerated_files=10000

$hitRate = $status['opcache_statistics']['opcache_hit_rate'];

<?php
$status = opcache_get_status(false);

$hitRate = $status['opcache_statistics']['opcache_hit_rate'];
$memory = $status['memory_usage'];
$cachedFiles = $status['opcache_statistics']['num_cached_scripts'];
$maxFiles = ini_get('opcache.max_accelerated_files');

printf("Hit rate: %.2f%%\n", $hitRate);
printf("Free memory: %.1f MB\n", $memory['free_memory'] / 1_048_576);
printf("Wasted memory: %.1f MB\n", $memory['wasted_memory'] / 1_048_576);
printf("Cached files: %d / max_accelerated_files=%s\n", $cachedFiles, $maxFiles);

Cross-reference

For the full internals — how opcodes are generated and stored, the shared memory allocator, how cache invalidation actually detects changed files, and a complete walkthrough of setting up preloading — see the OPcache and JIT lesson. This lesson intentionally stayed narrow: the goal was 'what do I check and tune when I'm trying to improve throughput', not 're-explain how the cache works'.

opcache.validate_timestamps = 1; left at default in production opcache.validate_timestamps = 0; and run opcache_reset() (or restart php-fpm) from the deploy pipeline on every release

Leaving timestamp validation on in production means every request pays a filesystem stat() call per included file to check for changes — exactly the kind of I/O overhead OPcache is meant to eliminate. Turning it off and invalidating explicitly on deploy gets the full performance benefit safely.

In production, what is the standard practice for opcache.validate_timestamps and why? A. Leave it at its default of 1, so changes are always picked up immediately B. Set it to 0, and invalidate the cache explicitly (e.g. opcache_reset()) as part of the deploy process C. Disable OPcache entirely to avoid the setting mattering D. Set it to 0 only in local development, and 1 everywhere else

B. Set it to 0, and invalidate the cache explicitly (e.g. opcache_reset()) as part of the deploy process — Setting validate_timestamps=0 removes a per-file filesystem stat() check on every request, which is real overhead at scale. The tradeoff — deploys not being picked up automatically — is handled by invalidating the cache explicitly during deployment.

Your production app has more PHP files (including vendor dependencies) than opcache.max_accelerated_files allows. What is the likely performance symptom? A. PHP-FPM refuses to start B. OPcache silently evicts and recompiles files on a rotating basis, degrading toward uncached performance C. JIT compilation is automatically disabled D. Memory usage drops to zero

B. OPcache silently evicts and recompiles files on a rotating basis, degrading toward uncached performance — When the file count exceeds max_accelerated_files, OPcache starts evicting cached entries even if raw memory isn't exhausted, causing some files to be recompiled repeatedly — a common silent cause of degraded throughput.