Every time PHP runs a script, it has to read the file, parse it and compile it to opcodes before it can execute anything. OPcache stores those compiled opcodes in shared memory, so PHP-FPM workers skip that work on every following request. It is enabled by default on Ubuntu 24.04, but its default limits are too small for large applications such as WordPress with many plugins, Laravel, Symfony or Magento. In this tutorial you will check OPcache, size it for your code base, choose how it detects code changes, clear it safely during deployments and monitor its hit rate.

Prerequisites

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • PHP 8.3 with PHP-FPM serving a PHP application (for example behind Nginx). Install it with sudo apt install php8.3-fpm if needed.

This guide uses the PHP 8.3 paths from Ubuntu 24.04. If you run another PHP version, replace 8.3 in paths and service names.

Step 1 - Checking that OPcache is enabled

OPcache is packaged as php8.3-opcache and installed together with PHP-FPM. Make sure it is present:

sudo apt install php8.3-opcache

Check the version banner of PHP. The last line confirms that the extension is loaded:

php -v
PHP 8.3.6 (cli) (built: Jul 14 2026 18:30:55) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.6, Copyright (c) Zend Technologies
    with Zend OPcache v8.3.6, Copyright (c), by Zend Technologies

The command-line PHP and PHP-FPM read different configuration directories (/etc/php/8.3/cli/ and /etc/php/8.3/fpm/), and OPcache is disabled for the CLI by default. What matters for your website is the FPM configuration. Show its current OPcache settings:

php-fpm8.3 -i | grep -E "^opcache\.(enable|memory_consumption|interned_strings_buffer|max_accelerated_files|validate_timestamps|revalidate_freq|jit_buffer_size) "
opcache.enable => On => On
opcache.interned_strings_buffer => 8 => 8
opcache.jit_buffer_size => 0 => 0
opcache.max_accelerated_files => 10000 => 10000
opcache.memory_consumption => 128 => 128
opcache.revalidate_freq => 2 => 2
opcache.validate_timestamps => On => On

These are the built-in defaults: 128 MB of shared memory, 8 MB for interned strings, room for 10,000 files, and a check for changed files at most every 2 seconds.

Step 2 - Adding a status script to measure OPcache

Before changing anything, set up a way to see how full OPcache is. OPcache lives in the memory of the PHP-FPM master process, so the status must be read through PHP-FPM, not with the php command line.

Create a small status script outside the web root so it is never reachable from the internet:

sudo nano /var/www/opcache-status.php
<?php
$s = opcache_get_status(false);
if ($s === false) {
    exit("OPcache is disabled\n");
}
$mem = $s['memory_usage'];
$str = $s['interned_strings_usage'];
$st  = $s['opcache_statistics'];

printf("Memory used:      %.1f MB of %.1f MB (wasted %.1f%%)\n",
    $mem['used_memory'] / 1048576,
    ($mem['used_memory'] + $mem['free_memory'] + $mem['wasted_memory']) / 1048576,
    $mem['current_wasted_percentage']);
printf("Interned strings: %.1f MB of %.1f MB\n",
    $str['used_memory'] / 1048576, $str['buffer_size'] / 1048576);
printf("Cached scripts:   %d (keys %d of %d)\n",
    $st['num_cached_scripts'], $st['num_cached_keys'], $st['max_cached_keys']);
printf("Hit rate:         %.2f%% (hits %d, misses %d)\n",
    $st['opcache_hit_rate'], $st['hits'], $st['misses']);
printf("Restarts:         oom %d, hash %d, manual %d\n",
    $st['oom_restarts'], $st['hash_restarts'], $st['manual_restarts']);
printf("JIT:              %s\n", !empty($s['jit']['on']) ? 'on' : 'off');

Install cgi-fcgi, a small tool that sends a request straight to the PHP-FPM socket:

sudo apt install libfcgi-bin

Run the script through PHP-FPM:

sudo env SCRIPT_FILENAME=/var/www/opcache-status.php REQUEST_METHOD=GET cgi-fcgi -bind -connect /run/php/php8.3-fpm.sock
X-Powered-By: PHP/8.3.6
Content-type: text/html; charset=UTF-8

Memory used:      121.4 MB of 128.0 MB (wasted 4.2%)
Interned strings: 8.0 MB of 8.0 MB
Cached scripts:   9981 (keys 10364 of 16229)
Hit rate:         97.10% (hits 1482117, misses 44213)
Restarts:         oom 2, hash 0, manual 0
JIT:              off

This example shows a cache that is too small: memory and interned strings are full, and oom 2 means OPcache already ran out of memory twice and had to discard everything. Your numbers will look different, but these are the values to watch. Browse your site or let it receive traffic for a while before reading them, because the cache is filled lazily as scripts are requested.

Step 3 - Sizing memory and the file limit

Count the PHP files that your application can load, including the vendor directory and plugins. Replace /var/www/your_app with your application's directory:

find /var/www/your_app -type f -name '*.php' | wc -l
14523

Use this number and the status output to choose the values:

  • opcache.max_accelerated_files: set it above the number of PHP files, with room to grow. PHP rounds the value up to the next number in a fixed list of primes (for example 16229, 32531, 65407), which is why the status showed a maximum of 16229 keys for the default of 10000. For 14,523 files, 20000 (which becomes 32531) is a good choice.
  • opcache.memory_consumption: the shared memory for compiled code, in MB. Start at 256 for large applications and raise it if free_memory stays near zero or oom_restarts increases. Small sites are fine with 128.
  • opcache.interned_strings_buffer: memory in MB for identical strings (class names, array keys) shared between workers. Frameworks fill the default 8 MB quickly; 16 to 32 is common.

Create a separate configuration file for your settings. A file in conf.d with a high number is loaded after the package defaults and is not touched by package upgrades:

sudo nano /etc/php/8.3/fpm/conf.d/99-opcache-tuning.ini
; OPcache tuning for PHP-FPM
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=20000

; Check changed files at most every 60 seconds
opcache.validate_timestamps=1
opcache.revalidate_freq=60

Keep opcache.save_comments at its default of 1. Many frameworks and libraries (Doctrine, PHPUnit, Laravel and Symfony attributes and annotations) read doc comments at runtime and break if they are stripped.

Restart PHP-FPM so that the master process creates a new shared memory segment with the new size. A restart briefly interrupts requests; on a busy site, do it during a quiet period:

sudo systemctl restart php8.3-fpm

Confirm that the new values are active:

php-fpm8.3 -i | grep -E "^opcache\.(memory_consumption|interned_strings_buffer|max_accelerated_files|revalidate_freq) "
opcache.interned_strings_buffer => 32 => 32
opcache.max_accelerated_files => 20000 => 20000
opcache.memory_consumption => 256 => 256
opcache.revalidate_freq => 60 => 60

After the site has warmed up again, run the status script from Step 2. You want free memory left over, oom at 0 and a hit rate above 99 percent once the cache is warm.

Step 4 - Choosing how OPcache detects code changes

By default OPcache checks the timestamp of each file every opcache.revalidate_freq seconds and recompiles it when it changed. That costs a stat() call per file per interval, which is small but not free. You have two sensible strategies:

SettingBehaviourUse it when
validate_timestamps=1, revalidate_freq=60Changes appear within a minute without any actionFiles are edited in place, for example WordPress updating plugins itself
validate_timestamps=0Files are never rechecked; code only changes after a reload or resetCode is only changed by a deployment process you control

For development, set opcache.revalidate_freq=0 so every change is visible immediately.

With validate_timestamps=0, PHP never notices edited files on its own. Only use it if every deployment clears the cache as shown in the next step, and never on sites where the application updates its own code (WordPress, Drupal or Joomla installing updates from the admin panel).

To use it, change the two lines in your tuning file:

sudo nano /etc/php/8.3/fpm/conf.d/99-opcache-tuning.ini
opcache.validate_timestamps=0
opcache.revalidate_freq=0

Then apply the change:

sudo systemctl reload php8.3-fpm

Step 5 - Clearing OPcache during deployments

When you deploy new code, OPcache may still hold the old version of the files. The simplest and most reliable way to clear it is a graceful reload of PHP-FPM, which lets requests in progress finish, starts new workers and empties the cache:

sudo systemctl reload php8.3-fpm

Add this command as the last step of your deployment, after the new code is in place. If your deployment switches a current symlink to a new release directory, the reload is also what makes PHP resolve the symlink to the new path.

Resetting from the command line with php -r 'opcache_reset();' does not work: it resets the separate, empty cache of the CLI process, not the cache of PHP-FPM. If you cannot reload the service, run opcache_reset() through the PHP-FPM socket with the same cgi-fcgi technique as in Step 2, using a script outside the web root.

Verify the reset worked by running the status script right after the reload. The number of cached scripts starts low again and grows as requests arrive:

sudo env SCRIPT_FILENAME=/var/www/opcache-status.php REQUEST_METHOD=GET cgi-fcgi -bind -connect /run/php/php8.3-fpm.sock | grep "Cached scripts"
Cached scripts:   1 (keys 1 of 32531)

The single cached script is the status script itself.

Step 6 - Enabling the JIT compiler (optional)

PHP 8 includes a JIT compiler that turns hot code paths into machine code. It gives large gains for CPU-heavy work (image processing, math, parsers) but little for typical web applications, which spend most of their time waiting for the database. In PHP 8.3 the JIT is off because opcache.jit_buffer_size defaults to 0.

To try it, add these lines to your tuning file:

sudo nano /etc/php/8.3/fpm/conf.d/99-opcache-tuning.ini
opcache.jit=tracing
opcache.jit_buffer_size=64M

Restart PHP-FPM, because the JIT buffer is allocated at startup:

sudo systemctl restart php8.3-fpm

Run the status script and confirm that the last line shows JIT: on. Then compare response times and CPU usage of your real application before and after. If you do not see a clear improvement, remove the two lines again: the JIT uses extra memory and is one more component that can misbehave with some extensions.

Step 7 - Cleaning up and monitoring

The status script lives outside the web root, so it is not exposed, but you can remove it once you no longer need it:

sudo rm /var/www/opcache-status.php

If you keep it for monitoring, check these values after deployments and whenever performance changes:

  • Free memory and oom restarts: if memory is full or oom_restarts grows, increase opcache.memory_consumption.
  • Keys used versus maximum: if num_cached_keys approaches max_cached_keys, increase opcache.max_accelerated_files.
  • Interned strings: if the buffer is full, increase opcache.interned_strings_buffer.
  • Wasted memory: memory left behind by outdated scripts. OPcache restarts itself when it exceeds opcache.max_wasted_percentage (5 percent by default) and memory is full. A reload after each deployment keeps it low.
  • Hit rate: should be above 99 percent on a warm cache. A lower rate on a site that is not being deployed usually means the cache is too small.

Troubleshooting

Changes to PHP files do not appear on the site. With validate_timestamps=0 this is expected; reload PHP-FPM. With the default settings, wait for revalidate_freq seconds or reload.

Settings changed in /etc/php/8.3/cli/php.ini have no effect on the website. The CLI and PHP-FPM use separate configuration files. Put OPcache settings in /etc/php/8.3/fpm/conf.d/ and check them with php-fpm8.3 -i, not php -i.

opcache_get_status() returns false in the command line. OPcache is disabled for the CLI (opcache.enable_cli=0). Read the status through PHP-FPM as shown in Step 2.

PHP-FPM fails to start after editing the tuning file. Run sudo php-fpm8.3 -t and sudo journalctl -u php8.3-fpm -n 30 to see which line is invalid.

Conclusion

You checked OPcache on Ubuntu 24.04, sized its memory, interned strings and file limit from your own code base, chose between timestamp validation and explicit resets, integrated a reload into your deployments and learned which status values to watch. OPcache is one of the cheapest performance improvements for any PHP site, as long as it is large enough for the code it has to hold.

As next steps, tune the PHP-FPM pool so the number of workers matches your server's memory, add an object cache such as Redis for database-heavy applications, and consider OPcache preloading (opcache.preload) for frameworks that support it.