How do I stop OPcache from causing 500s during a zero-downtime DeployerPHP deploy?
Question
We deploy with DeployerPHP: `releases/X` is created, `composer install` runs, assets are built, and finally the `current` symlink points at the new folder. On that symlink flip, old code lingers in PHP-FPM's OPcache (cache poisoning) and we get 500s. How do I safely wire `php-fpm reload` and OPcache clearing into the pipeline?
Answer
Short answer: flipping the symlink is atomic but not enough — after the flip you must gracefully reload PHP-FPM, and never leave OPcache stuck on old code after a release.
What you’re hitting is classic OPcache cache poisoning: OPcache keys compiled bytecode by file path. Since the current symlink’s own path never changes, workers keep serving the previous release’s bytecode; when some workers see the old realpath and others the new one, paths mix and you get 500s.
- Cleanest path: gracefully reload PHP-FPM after the symlink.
kill -USR2 <fpm-master>orsystemctl reload php-fpmmakes FPM pick up the newrealpathand refresh OPcache. Reload, not restart: in-flight requests finish, new workers compile the new code, no downtime. In Deployer, hook this onto thedeploy:symlinkstep. - Alternative: call
opcache_reset()from a deploy hook. Hitting the FPM socket withcachetoolis cleaner than triggering a reset endpoint over the web — CLI and FPM keep separate OPcaches, so resetting viaphp artisanwon’t touch FPM. A reload already covers this, so you rarely need it on top. - Tuning
opcache.revalidate_freqalone won’t fix it. It helps when the same file path changes; but across releases the path differs, and revalidate won’t save you. At a release boundary, a reset/reload is the guaranteed fix. - Octane is a different story entirely. Under Octane, code is served from memory, not disk; long-lived workers hold old classes in RAM. After the symlink flip you must run
php artisan octane:reloadto refresh the workers — otherwise no amount of OPcache work helps, the old code stays up.
Bottom line: make the flow explicit — flip the symlink → gracefully reload PHP-FPM (or octane:reload if you’re on Octane) → optionally opcache_reset. Hook it onto the symlink step in Deployer and never leave OPcache pinned to old code after a release. The deploy-side cost of Octane holding code in memory is its own hub topic; know that persistent-process model well and you won’t skip this reload step.
Related Reading
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.