Diagnosing Zend OPcache Problems in Production PHP: Stale Code, Low Hit Rates, and Memory Pressure
A diagnostic guide to Zend OPcache in production: reading opcache_get_status(), fixing stale deploys and low hit rates, and knowing when to escalate.
15 Aug 2026, 06:45 UTC

You deployed a fix twenty minutes ago and users still hit the old code path. Or your php-fpm logs show "OPcache is full" warnings while CPU climbs because PHP keeps recompiling scripts it should have cached. Both symptoms point at Zend OPcache, the opcode cache bundled with PHP 7.4 through 8.x, and both are diagnosable in a few minutes if you read its status output correctly.
This guide assumes PHP 7.4–8.x with the bundled OPcache extension under php-fpm or mod_php. Exact defaults differ between versions, so confirm values against the PHP manual for your installed release.
Recognizable conditions
Four patterns account for most OPcache incidents:
- Code changes don't appear after deploy. Almost always timestamp validation is disabled and nothing invalidated the cache.
- "OPcache is full" or rising CPU from recompilation. The cache ran out of memory or file slots, so scripts are evicted and recompiled constantly.
- Intermittent 500s right after deployments. A half-updated cache serving a mix of old and new file versions.
- CLI scripts behave as if uncached.
opcache.enable_clidefaults to off; this is expected, not a bug.
Cause and diagnostic table
| Finding | Likely cause | Confirm with |
|---|---|---|
| Hit rate below ~99% | memory_consumption or interned_strings_buffer too small; scripts evicted | opcache_get_status(): used_memory near the limit, restarts climbing |
| Cached scripts capped below project file count | max_accelerated_files too low | num_cached_scripts pinned at the configured max |
| Stale code after deploy | validate_timestamps=0 with no reset in the deploy pipeline | php.ini plus deploy script review |
| Stale code for a few seconds only | revalidate_freq delay outlasts the deploy window | Setting value vs. observed staleness duration |
| CLI shows empty cache | opcache.enable_cli=0 (default) | php -i | grep opcache.enable_cli |
Ordered checks
- Confirm the extension is loaded. Run
php -vandphp -m | grep -i opcacheon the server. You need shell access; no special privileges required. - Read status from the serving SAPI, not CLI. OPcache is per-process, and
opcache_get_status()from CLI does not reflect the php-fpm cache. Expose a protected endpoint (behind auth, internal network only) that callsopcache_get_status(false)and returns the JSON. Thefalseargument skips the full script list, keeping the response small. - Compare usage against limits. Check
memory_usage.used_memoryagainstopcache.memory_consumption, andnum_cached_scriptsagainstopcache.max_accelerated_files. A healthyopcache_hit_rateis typically above 99%. - Review deploy hooks. Look for
opcache_reset(),opcache_invalidate(), or a graceful php-fpm reload (systemctl reload php-fpmor equivalent) in the deployment pipeline. If timestamps validation is off and none of these exist, that is your stale-code cause. - Correlate with logs. Match cache restarts or "full" warnings in the php-fpm error log against deploy times and traffic spikes.
Fixes tied to findings
Low hit rate / evictions: raise limits with headroom above observed usage. Example php.ini changes:
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.interned_strings_buffer=16
Count your project's PHP files (find /var/www/app -name '*.php' | wc -l) and set max_accelerated_files comfortably above it. These changes require a php-fpm restart, which drops the cache — schedule accordingly.
Stale code after deploy: either add invalidation to the deploy step or re-enable validation. For zero-downtime deploys with validate_timestamps=0, call opcache_reset() from a web-context endpoint or gracefully reload php-fpm after the new code is in place. On lower-traffic environments, the simpler option is:
opcache.validate_timestamps=1
opcache.revalidate_freq=2
This trades a small amount of performance for automatic freshness within two seconds.
Multi-server caveat: each php-fpm pool, container, or server holds its own cache. A reset on one node does nothing for the others — run invalidation everywhere the code ships.
Verifying the fix
After resizing, watch the status endpoint through a traffic window: hit rate should settle above 99% and used_memory should plateau below the limit rather than pinning against it. After adding deploy invalidation, push a trivial visible change (a comment rendered in a footer, a version string) and confirm it appears immediately on every node.
When to escalate
Handle sizing and invalidation yourself; escalate when you see persistent hit-rate degradation after resizing with clear headroom, segfaults or memory corruption that disappear when OPcache is temporarily disabled (test on one node, not the whole fleet), or interactions between preloading and JIT settings on PHP 8.x that produce crashes. Those point at engine-level bugs or platform constraints and belong with whoever owns the PHP runtime build.
Limitations
Status output is a point-in-time snapshot of one process's shared cache; on multi-pool fpm setups, check each pool. Default values for memory, interned strings, and JIT vary across PHP 7.4 and 8.0–8.3, so treat any number quoted here as a starting point and verify against your version's manual.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.