skip to content

After a PHP deploy on servers with opcache.validate_timestamps=0, the site keeps serving the old code; why, and how should the deploy clear OPcache?

level: seniorimportance: should knowfreq 40%

answer

  1. the cache belongs to the server process
  2. CLI opcache_reset() hits a different cache
  3. reset is scheduled, not instant
  4. opcache.restrict_api guards the functions
  5. recompile burst after every reset

basics

~20 s

With validation off, OPcache never rereads changed files, so old compiled scripts keep running. Clear the cache inside its owner: call opcache_reset() through the web server's PHP, or restart or reload PHP-FPM; a CLI opcache_reset() cannot reach it.

solid answer

~50 s

`opcache.validate_timestamps=0` tells OPcache never to check the disk, so new files are invisible until the cache is cleared. The classic mistake is adding `php -r 'opcache_reset();'` to the deploy script: that runs in a separate CLI process whose cache — absent with the default `opcache.enable_cli=0`, in which case the call just returns `false` — is not the PHP-FPM master's shared memory. Working options: restart or gracefully reload PHP-FPM, which recreates the segment; or call `opcache_reset()` from inside the pool, through an internal endpoint or a FastCGI request, locked down with `opcache.restrict_api`. A reset is **scheduled**: the cache is emptied once no request is using it, so in-flight requests finish on the old code. Expect a short CPU burst while every worker recompiles, and remember that long-running CLI workers keep their old code in memory until they are restarted.

code

php · 10 lines
php
<?php
// /srv/ops/opcache-reset.php, reachable only from the deploy host;
// php.ini: opcache.restrict_api=/srv/ops/
declare(strict_types=1);

header('Content-Type: application/json');
echo json_encode([
    'reset' => opcache_reset(), // true = restart scheduled
    'pending' => opcache_get_status(false)['restart_pending'] ?? null,
]);

go deeper

for a junior

Recall that with validation off, deployed files are ignored until OPcache is reset or PHP-FPM is restarted.

for a middle

Explain why a CLI call to opcache_reset() cannot reach PHP-FPM's shared memory, and list the ways that do.

for a senior

Design the deploy sequence: release directory, switch, in-server reset or reload, worker restarts, verification, and a staggered reset to absorb the recompile burst.

for a principal

Choose between in-place resets and replacing instances per release, trading deploy speed against cold-cache warm-up and operational simplicity.

## Why the old code keeps running With `opcache.validate_timestamps=0`, OPcache compiles each file once and then serves the cached opcodes forever; it never compares the file on disk with what it has. Replacing the files therefore changes nothing for a running server. That is the intended behaviour of the setting — the deploy is expected to clear the cache — and the symptom appears when nobody built that step. ## Where the cache lives The cache is a **shared memory segment created by the server process at startup** and used by the processes started from it. Under PHP-FPM that is the master process and the workers it forks. Everything that talks to the cache must run inside that process tree. That explains the most common failed fix: ```bash php -r 'opcache_reset();' # in the deploy script: does not touch PHP-FPM's cache ``` A `php` command is a new CLI process. With the default `opcache.enable_cli=0` it has no cache at all and `opcache_reset()` returns `false`; with the CLI cache enabled, it resets its own private segment, which disappears when the command exits. PHP-FPM's cache is untouched either way. ## Ways that work | Approach | How it clears the cache | Watch out for | |---|---|---| | Restart or gracefully reload PHP-FPM | the master starts again with a new segment | brief capacity dip; the reload mechanics belong to the FPM configuration | | `opcache_reset()` from inside the pool | an internal URL or a FastCGI request runs the call in a worker | must be protected; restrict it with `opcache.restrict_api` | | `opcache_invalidate($file, true)` per changed file | drops single entries | misses deleted and renamed files and regenerated autoload maps | | New server or container per release | the new instance starts with an empty cache | none for OPcache; the cache warms on first traffic | `opcache.restrict_api` takes a path prefix; OPcache API functions called from scripts outside it return `false` with a warning, so only the reset script under that prefix can clear the cache. ## What opcache_reset() actually does 1. It **schedules** a restart of the cache and returns `true`, or `false` if OPcache is not active in this process or a restart is already pending or in progress. 2. The cache is emptied when no process is using it. Requests already running finish on the old code; new ones compile the new code. 3. If some process holds the cache for too long, OPcache forces the restart after `opcache.force_restart_timeout` seconds (default `180`) by killing the processes that still hold it. 4. `opcache_get_status()` reports `restart_pending` and `restart_in_progress`, and counts resets in `opcache_statistics.manual_restarts`. ## Side effects to plan for - **Recompile burst.** After a reset every worker compiles the files it needs on its next requests. On a busy server this shows as a CPU spike and slower responses for a few seconds. Resetting servers one at a time behind the load balancer smooths it. - **Symlinked release directories.** Many deploys switch a `current` symlink to a new release directory. Cached entries and PHP's realpath cache can keep resolving the symlink to the previous release until they are cleared, so the switch still needs a reset or reload; some setups pass the resolved release directory to PHP so every release has distinct paths. - **Several servers.** Each host has its own cache. A reset must reach every host behind the load balancer, and a host that missed it keeps serving the previous release indefinitely. - **Long-running PHP processes.** A queue consumer or daemon started before the deploy has the old classes loaded in its own memory. No OPcache operation changes that; those processes must be restarted. ## A deploy order that holds up 1. Upload the new release to a new directory. 2. Run migrations and build steps (autoload maps, caches). 3. Switch the web root to the new release. 4. Clear OPcache inside the server — reload PHP-FPM, or call the protected reset endpoint. 5. Restart long-running workers. 6. Check `opcache_get_status()` or a version endpoint to confirm the new code is live.

  • Why is opcache_invalidate() on the changed files not enough for a typical deploy?
    It only drops entries for the paths you pass. Deleted files stay cached, renamed classes leave stale entries, and regenerated autoload maps or config files are easy to miss. A full `opcache_reset()` or a PHP-FPM reload clears everything at once, which is safer for a release.
  • Is it safe to reset OPcache while requests are in flight?
    Yes. `opcache_reset()` only schedules the restart; the cache is emptied when no process is using it, so running requests finish with the code they started with. The cost is the recompile burst afterwards, not broken requests.
  • Why can a queue worker still run old code after OPcache was reset?
    A long-running PHP process loads its classes once, into its own memory, when it starts. Clearing OPcache affects future compilations only; the worker keeps executing what it already loaded until it is restarted.

saying these in an interview costs you the question

  • Running php -r 'opcache_reset();' in the deploy clears PHP-FPM's cache
  • opcache_reset() empties the cache instantly, breaking running requests
  • OPcache notices changed files eventually even with validation off
  • Resetting OPcache also restarts long-running queue workers
  • opcache_invalidate() on changed files covers deleted and renamed ones