Zend OPcache in Production: Sizing, Deploy Clears and the Counters That Prove It
OPcache caches compiled PHP opcodes in shared memory. Here is a production php.ini, how to size it from its restart counters, and why deploys go stale.
04 May 2026, 05:43 UTC

The short answer
Zend OPcache (listed as Zend OPcache in php -m) stores the compiled form of each PHP file in shared memory so later requests skip lexing, parsing and compilation. It is one of the highest-leverage changes available to a PHP deployment, and it is easy to get wrong in two specific ways: under-sizing the cache so it constantly discards itself, and setting opcache.validate_timestamps=0 without adding a cache clear to the deploy step.
Everything below assumes the Zend Engine shipped with PHP and the OPcache extension. Zend Framework (now Laminas), Zend Server and Zend Studio are separate products and are not covered here.
What actually gets skipped
On a cache miss, PHP reads the file from disk, tokenises it, builds a syntax tree, compiles that to opcodes, then executes them. On a cache hit, the first four steps are replaced by a lookup in a shared memory segment; only execution remains. The saving is CPU time in the compile phase. It does nothing for database queries, outbound HTTP calls or filesystem reads your application performs at runtime.
That distinction predicts where OPcache helps. A page dominated by database latency will show a small improvement. A page that includes hundreds of small classes and does little I/O will show a large one.
A worked php.ini configuration
These directives belong in the php.ini used by the PHP-FPM pool that serves the application, which is not necessarily the CLI php.ini.
; production starting point
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
; development only - re-check files every 2 seconds
; opcache.validate_timestamps=1
; opcache.revalidate_freq=2
memory_consumption is the size of the shared memory segment in megabytes. interned_strings_buffer holds deduplicated strings such as class and function names. max_accelerated_files sizes the hash table mapping file paths to cached entries; PHP rounds it up to the next suitable prime, so the effective value is not exactly the number you typed.
Why validate_timestamps=0 changes your deploy
With validate_timestamps=1, PHP checks each file's modification time at most once every revalidate_freq seconds and recompiles changed files. That is convenient and costs a stat call per file. With validate_timestamps=0, PHP never re-reads a file it has already cached. Edited code stays invisible until the cache is cleared.
Clearing options, roughly in order of bluntness:
opcache_reset()called through the web SAPI, for example from a protected script or a cache-management tool that reaches it over HTTP.- A PHP-FPM reload or restart.
- A full service restart, which also drops other warm state.
One trap: opcache_reset() run from the command line resets the CLI's own cache. The CLI and each FPM pool have separate shared memory segments, so a CLI reset does not clear what the web pool is serving. The reset has to reach the SAPI that holds the cache.
Sizing: the counters that matter
Call opcache_get_status() from a small health endpoint (protect it, since it exposes file paths) and read the restart counters. They are the primary evidence that the configuration is correctly sized.
| Counter | Meaning | Response |
|---|---|---|
oom_restarts | The segment filled and the whole cache was discarded | Raise opcache.memory_consumption |
hash_restarts | More scripts than the hash table can hold | Raise opcache.max_accelerated_files |
manual_restarts | Something called a reset | Expected on deploy; unexpected means a script or tool is clearing the cache |
cached_scripts vs max_cached_keys | Hash table fill level | Leave headroom; do not size to the exact file count |
Also watch the hit and miss counts in opcache_statistics. A rising miss count under steady traffic usually means the cache is being flushed, not that the code is changing.
Preloading and JIT: two features that need a reason
Preloading (PHP 7.4 and later) points opcache.preload at a script that compiles and links selected classes and functions into a permanent segment at startup, removing even the per-request cache lookup for that code. The cost is operational: changing the preload script requires a server restart, and a preload script that references something it cannot resolve can abort startup. Test it in staging first.
JIT (PHP 8.0 and later) is controlled by opcache.jit and sized by opcache.jit_buffer_size. Setting opcache.jit without allocating a buffer size leaves JIT effectively off, which is a frequent misconfiguration. JIT targets CPU-bound code; on a typical I/O-bound web request it often changes little, so measure before keeping it.
Common mistakes
- Sizing
max_accelerated_filesto the current file count. Vendor directories, generated proxies and future code all add files. - Turning off timestamp validation for the performance win and then forgetting the deploy step. The symptom is "the deploy did not take effect" while the files on disk are correct.
- Assuming a CLI reset clears the web cache, as described above.
- Treating OPcache as a data cache. It caches compiled code; query results and rendered output need APCu, Redis or an application-level cache.
- Ignoring containers. Each container has its own shared memory segment, so every node behind a load balancer needs the clear, and short-lived containers may never warm the cache at all.
How to check it is working
- Confirm the extension is loaded on the runtime that serves traffic:
php -m | grep -i opcache. This reads the CLI configuration, so confirm the web SAPI's effective values through aphpinfo()page oropcache_get_configuration(). - Watch
opcache_get_status()over a representative traffic window: hit rate, free memory and the restart counters. - Benchmark the same request path with
opcache.enable=0and=1under comparable load. If the difference is negligible, the workload is dominated by something else. - After a deploy, request a response containing a build or version marker. If it shows the old value, the clear did not reach the pool.
Directive defaults and availability differ across PHP minor versions, and preloading and JIT have minimum version requirements. Check the PHP manual and changelog for the exact version you are running before copying any of the values above.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.