OPcache, explained for people who have never turned it on

By Ricardo Ribeiro · · 5 min read · PHP, Performance

Two hundred requests, two hundred compiles

A while back I looked at a small site on shared hosting where a page took around 400 milliseconds before anything appeared on screen. Nothing in the code was slow and the database was fine. PHP was reading the same twenty files from disk on every request, parsing them, compiling them into opcodes, and throwing the result away when the request ended. The files had not changed between requests, so the work was identical every time. OPcache exists to stop that.

What OPcache actually does

PHP does not execute the text of your files. It reads a file, parses it and compiles it into opcodes, which are the instructions the engine actually runs. Without a cache that happens on every request, for every file loaded, including every Composer package in vendor.

OPcache keeps the compiled opcodes in shared memory. The next request that needs the same file gets the compiled version straight from memory. The file on disk is still the source of truth, it is just read less often.

It is a bytecode cache and nothing else. It does not hold query results, sessions or configuration.

Turning it on

In PHP 8.5 the extension is compiled into the binary, so there is nothing to install. On older versions you load it explicitly with a line like zend_extension=opcache.so, and most shared hosting panels have a switch. The settings that matter:

opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.revalidate_freq=0

The five settings in plain terms

  • opcache.enable turns it on for the web server. There is a separate opcache.enable_cli for command line scripts, usually left off because a script that runs once gains nothing from it.
  • opcache.memory_consumption is how much shared memory the cache may use, in megabytes. When it runs out, OPcache does not fail loudly: it discards entries and restarts, so a number that is too small shows up as restarts in the status output, not as an error.
  • opcache.max_accelerated_files is the number of files it can hold at once. Set it higher than the real number of PHP files in your project, otherwise the extra ones are recompiled on every request and everything looks fine while it is not.
  • opcache.validate_timestamps decides whether OPcache checks the modification time of the file before using the cached version. This is the one that changes how you deploy.
  • opcache.revalidate_freq is how many seconds pass between those checks. With a value of two, a change you saved a second ago may not be visible until the next check.

Why validate_timestamps decides your deploy

With validate_timestamps=1 PHP compares the timestamp of the file on disk with the one recorded next to the cached opcodes. Different means recompile. That is what you want on your machine: save the file, refresh the page, see the change.

In production it is the opposite. Every one of those checks is a filesystem call, and with a few thousand files it is time spent on every request for no benefit. The usual production value is validate_timestamps=0, which tells OPcache to trust what it has. The consequence is that a deploy does not take effect until the cache is cleared.

So the deploy order is: put the files in place, then clear the cache. Reloading PHP-FPM does that, because the workers share one OPcache and a reload gives them a fresh one. A small script that calls opcache_reset() works too, as long as you request it over HTTP. Running that script from the command line does not, because the CLI has its own separate cache.

<?php
// deploy/reset-opcache.php, delete it after the first request or protect it
opcache_reset();
echo 'cleared';

If you do this with a file in the public root, protect it or delete it right after, because anyone who finds it can clear your cache.

Checking that it is working

From the shell, php -i | grep -i opcache shows whether the extension is loaded, but it reflects the command line configuration, not the web one. The web SAPI is what matters, and it is easier to ask it directly:

<?php
$status = opcache_get_status(false);

if ($status === false) {
    echo 'OPcache is not enabled for this request';
    exit;
}

$stats = $status['opcache_statistics'];

echo 'cached scripts: ', $stats['num_cached_scripts'], PHP_EOL;
echo 'hit rate: ', round($stats['hit_rate'], 1), '%', PHP_EOL;
echo 'memory used: ', round($status['memory_usage']['used_memory'] / 1048576, 1), ' MB', PHP_EOL;
echo 'restarts: ', $stats['oom_restarts'], ' out of memory, ', $stats['hash_restarts'], ' hash', PHP_EOL;

After a few requests the hit rate should be close to 100 percent. Restarts above zero that keep climbing mean the memory or the file slots are too small.

What I set

On a project of any size I start with 256 MB of memory, a file limit comfortably above the real file count, and timestamps off in production with an explicit reset in the deploy script. On my own machine I leave timestamps on with a revalidate frequency of zero, so every save is picked up immediately. That combination takes a minute to set and has been right every time.

← All posts

Privacy · Cookies

Need a hand with a WordPress site that keeps breaking? See what I do and how I work.