Bali, Indonesia
Insights

Uploaded a PHP Fix but the Site Still Runs the Old Code? Check OPcache

October 27, 2026
0:00
You fixed a bug in a PHP file, uploaded it, and confirmed on the server that the file contains your change. The site still behaves exactly like before. Before you blame the upload tool or the browser, check OPcache, PHP's built-in bytecode cache. It is a common reason why a correct file on disk is not the code that actually runs. OPcache compiles each PHP script once and keeps the compiled result (bytecode) in shared memory, so PHP does not have to parse the file again on every request. Whether it notices your edit depends on two settings. opcache.validate_timestamps decides if PHP checks the file's modification time at all. opcache.revalidate_freq decides how many seconds pass between those checks. If timestamps are off, PHP never checks and keeps serving the old bytecode until someone resets the cache. If the frequency is set high, PHP picks up your change eventually, but late enough that it looks broken. This guide is about verifying that from the command line, not guessing. The steps are: prove the cached copy is the problem, read the settings that the serving PHP actually uses, reset the cache in the right place, and confirm the reset worked.
A site can have several caching layers, and OPcache is only one of them. Page caching plugins, LiteSpeed Cache, Varnish, and a CDN all store rendered HTML. OPcache stores compiled code. The symptoms differ, and that difference gives you a quick test. Request a page that a page cache will not serve. For WordPress, the admin area is usually not page-cached. Adding a random query string or sending a request with a logged-in cookie also bypasses many page caches. Check whether the old behavior is still there. If the uncached request shows the fix, your PHP is fine and the problem is a page or CDN cache. If the uncached request still runs the old logic, the old code is being executed, and OPcache becomes the main suspect. Two other causes are worth a few seconds. First, confirm the file you edited is the one the site loads. Staging copies, a second document root, or a theme that has a child-theme override often cause confusion. Second, check that the file's modification time actually changed. Some upload tools preserve the original mtime. With validate_timestamps on, PHP compares against that time, so a preserved mtime looks like no change.
This step causes most of the confusion at the support desk. Running php -i | grep opcache over SSH shows the settings of the command-line PHP. The CLI usually has OPcache disabled (opcache.enable_cli is off by default) and often reads a different php.ini. It tells you nothing about the PHP-FPM, LSAPI, or Apache module that handles web requests. For the same reason, php -r 'opcache_reset();' from a shell only clears the CLI's own cache, not the one your site uses. Find the handler first, then read its configuration and any per-pool overrides:
BASH
# Which PHP processes are running? (php-fpm, lsphp, or Apache with mod_php)
ps aux | grep -E '[p]hp-fpm|[l]sphp|[h]ttpd|[a]pache2'

# Debian/Ubuntu PHP-FPM: base settings plus pool overrides (adjust the version)
grep -rnE 'opcache[.](enable|validate_timestamps|revalidate_freq)' /etc/php/8.2/fpm/

# cPanel EA-PHP: php.ini, php.d and php-fpm.d live under the same root
grep -rnE 'opcache[.](enable|validate_timestamps|revalidate_freq)' /opt/cpanel/ea-php82/root/etc/

# Per-directory overrides inside the site itself
grep -nE 'opcache' /path/to/docroot/.user.ini /path/to/docroot/.htaccess 2>/dev/null
If grep finds nothing, PHP uses the compiled-in defaults: timestamps validated, and a revalidate frequency of a couple of seconds. A pool file may also contain php_admin_value[opcache.validate_timestamps] = 0. That line wins over php.ini for that pool, so do not stop at the first match. Paths vary by distro and panel, and RHEL-family systems typically use /etc/php.ini and /etc/php.d/. Some panels write per-domain overrides to their own locations. Treat the grep output as a strong hint, and confirm it with the next step.
The reliable answer comes from inside the web SAPI (the interface between PHP and the web server, such as FPM or LSAPI). A small temporary script reports the effective settings and compares your edited file's mtime against the copy OPcache holds. Give the file a hard-to-guess name and delete it when you are done, because it reveals configuration details.
PHP
<?php
// opcache-check-RANDOM.php: temporary diagnostic, delete after use
header('Content-Type: text/plain');
$target = __DIR__ . '/path/to/edited-file.php'; // the file you changed

echo 'SAPI: ', PHP_SAPI, PHP_EOL;
echo 'Loaded php.ini: ', php_ini_loaded_file(), PHP_EOL;
foreach (['opcache.enable', 'opcache.validate_timestamps', 'opcache.revalidate_freq', 'opcache.restrict_api'] as $key) {
    echo key, ' = ', var_export(ini_get(key), true), PHP_EOL;
}

if (!function_exists('opcache_get_status')) {
    exit('OPcache extension is not loaded' . PHP_EOL);
}
$status = opcache_get_status(true);
if ($status === false) {
    exit('OPcache is disabled or restricted (check opcache.restrict_api)' . PHP_EOL);
}

real = realpath(target);
if ($real === false) {
    exit('Target file not found: ' . $target . PHP_EOL);
}
echo 'Target: ', $real, PHP_EOL;
echo 'File mtime on disk: ', date('c', filemtime($real)), PHP_EOL;

if (isset(status['scripts'][real])) {
    s =status['scripts'][$real];
    echo 'Cached: yes, hits = ', $s['hits'], PHP_EOL;
    echo 'Cached copy based on mtime: ',
        isset(s['timestamp']) ? date('c',s['timestamp']) : 'not tracked (validate_timestamps is off)',
        PHP_EOL;
} else {
    echo 'Cached: no', PHP_EOL;
}
The output tells you three things. The SAPI line confirms you are looking at the web PHP and not the CLI, and it should say something like fpm-fcgi or litespeed. The settings show what is in effect for this site after every override. The comparison is the evidence. If the cached copy is based on an older mtime than the file on disk, or the timestamp is reported as not tracked and the file is cached, OPcache is serving the old version. If the file is not cached at all, or both times match, look elsewhere: a different file, a page cache, or an application-level cache.
A reset only helps if it hits the same shared memory that the web requests use. That rules out the CLI. With root access, reloading or restarting the PHP handler is the simplest option. For PHP-FPM, a reload typically clears OPcache. A restart is the fallback if the reload does not. Keep in mind that every site served by that FPM master loses its cache and warms it up again, so expect a short dip in performance on a busy server. Without root, which is the normal case on shared hosting, you can clear the cache from inside the site. Add opcache_invalidate($real, true); to the diagnostic script to drop just the edited file, or call opcache_reset(); to clear the whole cache for that process group. Both only work if opcache.restrict_api allows calls from the script's path. If it does not, use the panel's PHP restart option if it has one, or ask your host. Under LiteSpeed, how long lsphp processes and their cache live depends on the server configuration, so check your own panel or server documentation instead of assuming. Then verify the result instead of trusting the reset:
BASH
# Before: note the cached timestamp vs the file mtime
curl -s https://example.com/opcache-check-RANDOM.php

# Reset (root only; service name varies: php8.2-fpm, php-fpm, ea-php82-php-fpm)
sudo systemctl reload php8.2-fpm

# After: target should show 'Cached: no', or a timestamp matching the file mtime
curl -s https://example.com/opcache-check-RANDOM.php

# Remove the diagnostic script
rm /path/to/docroot/opcache-check-RANDOM.php
Right after a reset, the target usually shows as not cached until something requests it. Load the real page once and run the check again. The cached copy should now carry the current mtime, and the site should behave like the new code.
Turning off opcache.validate_timestamps is a deliberate performance choice. PHP skips a filesystem stat call on every include, which adds up on sites with many files. The trade-off is that every deploy has to include a cache reset. That works well when code only changes through a pipeline that reloads PHP afterwards. It works badly when people edit files over FTP or the file manager, or install plugins through the dashboard. WordPress updates fall into that last group too: core and plugin updates write new files, and with timestamps off they may not take effect until the next reset. If nobody on the account deploys through a pipeline, the safer setup is usually timestamps on with a small opcache.revalidate_freq. That is close to the default behavior, and edits show up within a few seconds. If you want to keep timestamps off, put the reload or opcache_invalidate call in the deploy script itself, so it does not depend on someone remembering. Teams that switch releases with symlinks should also test that the switch is picked up. Path resolution caching can keep the old release running until PHP is reloaded, depending on the version and configuration. The practical habit is simple. When an edit does not take effect, first prove which layer is stale with an uncached request. Then ask the web SAPI itself what OPcache holds, and reset it there instead of in the shell. Two curl requests before and after the reset tell you more than an afternoon of re-uploading the same file.