# How do I stop OPcache from causing 500s during a zero-downtime DeployerPHP deploy?

> After flipping the symlink, gracefully reload PHP-FPM rather than restarting it; on Octane, octane:reload is mandatory and opcache_reset is optional.

- Asked: 2026-06-10
- Answered: 2026-06-12
- Asked by: Şükrü
- Tags: ci-cd, laravel
- Source: https://www.muhammetsafak.com.tr/en/just-ask/zero-downtime-deployment-with-deployerphp-and-opcache/
- Language: en-US
- Author: Muhammet Şafak

---
**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?


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.

1. **Cleanest path: gracefully reload PHP-FPM after the symlink.** `kill -USR2 <fpm-master>` or `systemctl reload php-fpm` makes FPM pick up the new `realpath` and refresh OPcache. **Reload, not restart:** in-flight requests finish, new workers compile the new code, no downtime. In Deployer, hook this onto the `deploy:symlink` step.
2. **Alternative: call `opcache_reset()` from a deploy hook.** Hitting the FPM socket with `cachetool` is cleaner than triggering a reset endpoint over the web — CLI and FPM keep separate OPcaches, so resetting via `php artisan` won't touch FPM. A reload already covers this, so you rarely need it on top.
3. **Tuning `opcache.revalidate_freq` alone 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.
4. **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:reload` to 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

- [Laravel Octane: the performance that comes with a persistent process](/en/blog/laravel-octane-persistent-process-performance/) — Blog
- [How do I run database migrations safely in CI/CD?](https://www.muhammetsafak.com.tr/en/just-ask/running-database-migrations-safely-in-ci-cd/) — Just Ask
- [Should I use chunkById instead of chunk when the same job also updates the rows it iterates?](https://www.muhammetsafak.com.tr/en/just-ask/should-i-use-chunkbyid-instead-of-chunk-when-the-same-job/) — Just Ask
- [My non-root container can't write to the mounted storage directory—how do I fix the permissions?](https://www.muhammetsafak.com.tr/en/just-ask/my-non-root-container-cant-write-to-the-mounted-storage-directory/) — Just Ask
