OPcache Serving Stale Code? The PHP-FPM Worker Problem
After a PHP deploy, some users still see old code. The cause is often OPcache isolation across PHP-FPM workers, not a failed deployment.
09 Jun 2026, 00:13 UTC

The Deploy That Only Half-Worked
You push a PHP change, clear the opcode cache, and reload the page. Most users see the fix, but a subset keeps seeing the old behavior. If your application runs under PHP-FPM, the likely culprit is not your deployment script. It is how OPcache stores bytecode.
Zend OPcache is PHP's built-in bytecode cache. It holds compiled opcodes (operation codes) in shared memory so the engine does not parse and compile the same script on every request. However, each PHP-FPM worker process maintains its own OPcache instance. Clearing the cache in the worker that handles your cache-clear request does not clear the bytecode sitting in other workers. Users routed to those workers continue executing the pre-deploy version.
Why One Reset Is Not Enough
PHP-FPM spawns multiple worker processes to handle concurrent requests. Each worker loads PHP into its own memory space, and OPcache's shared memory segment is private to that process. When opcache.validate_timestamps=0 is set, PHP stops checking file modification times entirely. That removes per-request filesystem overhead, but it also means a deployed file stays cached until the worker's cache is explicitly invalidated or the worker restarts.
This creates a partial invalidation problem: an HTTP endpoint that calls opcache_reset() only resets the worker that happens to serve that request. Behind a load balancer or even FPM's own process manager, the next user request may land in a different worker still holding the old bytecode.
Worked Example: Auditing and Clearing the Pool
Before changing anything, confirm how many workers your pool runs and whether OPcache is active.
- Log in to the application server as a user with read access to PHP configuration, typically via SSH.
- Locate the active configuration file:
Expected output is a path such asphp -i | grep "Loaded Configuration File"/etc/php/8.4/fpm/php.ini. The version number depends on your installed PHP release. - Confirm OPcache is enabled for FPM:
Look forphp -i | grep "opcache.enable"opcache.enable => 1. CLI and FPM can use different ini files, so also check the FPM-specific path if your distribution splits them. - Count the PHP files in your application to size the cache:
Use this number when settingfind /var/www/html -name "*.php" | wc -lopcache.max_accelerated_files; choose a prime above the count to reduce hash collisions.
To clear every worker safely, use a graceful restart of the PHP-FPM pool. This ends idle workers and replaces busy ones after they finish current requests.
sudo systemctl reload php8.4-fpm
Run as a user with sudo privileges. Replace 8.4 with your installed PHP-FPM service name. Risk: active requests may be briefly interrupted during the reload window. If you need zero interruption, schedule a rolling restart across nodes instead of restarting the whole pool at once.
If you prefer not to restart, create a protected cache-clear endpoint and call it repeatedly during deployment until all workers have been hit. Because you cannot choose which worker handles each HTTP request, this is probabilistic and less reliable than a restart.
Verification and Limitations
After the restart, verify the cache state from a web request, not CLI. CLI invocations use a separate OPcache instance and will not show web-server cached bytecode.
Create a temporary diagnostic file or add a guarded route that calls:
<?php
$status = opcache_get_status();
var_dump($status['opcache_enabled']);
var_dump($status['cache_full']);
var_dump($status['num_cached_scripts']);
var_dump($status['memory_usage']);
?>
Expected checks: opcache_enabled is true, cache_full is false, and num_cached_scripts climbs toward your total PHP file count after warmup. If cache_full is true, increase opcache.memory_consumption in the FPM php.ini and reload the pool.
Limitation: With validate_timestamps=0, edited files do not take effect until you invalidate or restart. OPcache also does not cache database results or API responses; those require a separate data cache such as Redis. Finally, OPcache stores bytecode only, not compiled HTML or rendered views.
Actionable Closing
For production PHP-FPM deployments, set opcache.validate_timestamps=0 to remove filesystem stat calls, tune opcache.memory_consumption and opcache.max_accelerated_files to your codebase size, and make your deployment pipeline restart or gracefully reload the entire PHP-FPM pool after code changes. A single opcache_reset() call is not enough when multiple workers are running.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.