# 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://muhammetsafak.com/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.

## Short answer

What you're hitting is classic **OPcache cache poisoning**: OPcache keys compiled bytecode by file path. So the problem isn't your deploy tool but how code is held in memory — and on Octane the same issue goes one layer deeper; I explain the persistent-process model from the ground up in the [Octane post](/blog/laravel-octane-persistent-process-performance/).

## Why

1. **The `current` symlink's own path never changes.** So 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.

2. **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.

3. **Octane is a different story entirely.** Under Octane, code is served from **memory**, not disk; long-lived workers hold old classes in RAM. So no amount of OPcache work helps while the workers themselves aren't refreshed.

## What to do

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. **On Octane, run `php artisan octane:reload` after the symlink flip.** Refreshing the workers with that command is mandatory; otherwise the old classes stay in memory and nothing you do on the OPcache side matters.

**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](/blog/laravel-octane-persistent-process-performance/) — Blog
- [How do I run database migrations safely in CI/CD?](https://muhammetsafak.com/just-ask/running-database-migrations-safely-in-ci-cd/) — Just Ask
- [For comments across many models should I use a polymorphic relation or separate tables, given indexing and foreign keys?](https://muhammetsafak.com/just-ask/comments-polymorphic-relation-separate-tables-indexing-foreign-keys/) — Just Ask
- [Should I use chunkById instead of chunk when the same job also updates the rows it iterates?](https://muhammetsafak.com/just-ask/should-i-use-chunkbyid-instead-of-chunk-when-the-same-job/) — Just Ask
