OPcache and Preloading

What OPcache keeps in shared memory, why a deploy has to clear it, and the settings that differ between development and production.

Syntaxopcache_invalidate(string $filename, bool $force = false): bool

Before PHP can run a file it has to read it, split it into tokens, and compile those into opcodes, the instructions the engine actually executes. Without a cache it repeats that work on every request. OPcache stores the compiled opcodes in shared memory, so later requests skip reading and compiling and run the stored version. It ships with PHP and has been enabled by default since PHP 5.5.

Development against production

The setting that separates the two is opcache.validate_timestamps. With it on, which is the default, PHP compares a file's modification time, at most once every opcache.revalidate_freq seconds (default 2), and recompiles when it changed. That is what you want while editing. In production it is often set to 0: nothing is ever rechecked, so no time goes on stat calls, and an edited file is never noticed.

A deploy has to clear it

With timestamps off, new code does nothing until the cache is emptied. opcache_reset() empties all of it. opcache_invalidate($file, true) drops one file. Reloading PHP-FPM starts the workers with an empty cache. Deploying into a new directory and moving a symlink changes the paths, which sidesteps the opcode cache but not the realpath cache, a separate store of resolved paths sized by realpath_cache_size (4096K) and expiring after realpath_cache_ttl seconds (120).

Preloading

Since PHP 7.4, opcache.preload names a script that runs once at startup and calls opcache_compile_file() on your classes. They are then in memory for every request, and changing them needs a restart.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Explaining why code changed on the server but the site still serves the old behaviour.
  • Adding a cache reset or an FPM reload as the final step of a deploy script.
  • Sizing opcache.memory_consumption and max_accelerated_files for a project that has outgrown the defaults.
  • Cutting per-request class loading on a busy endpoint by preloading the application's classes.

More examples

Production settings against development settings

Three of these are sizes and two change behaviour. opcache.memory_consumption (default 128, in MB) and opcache.max_accelerated_files (default 10000) must be larger than what your project needs, because once the file slots are full OPcache stops caching anything new. validate_timestamps=0 is the line that makes a deploy step mandatory. In development, revalidate_freq=0 means the timestamp is checked on every request, so a save takes effect at once. opcache.memory_consumption (default 128, in MB) and opcache.max_accelerated_files (default 10000) must be larger than what your project needs, because once the file slots are full OPcache stops caching anything new. validate_timestamps=0 is the line that makes a deploy step mandatory. In development, revalidate_freq=0 means the timestamp is checked on every request, so a save takes effect at once.

Example · ini
; php.ini on a production server
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0

; the same two lines while you are editing code
; opcache.validate_timestamps=1
; opcache.revalidate_freq=0

A preload script

Preloading (PHP 7.4 and later) compiles these files once when PHP starts, and every request then has the classes already linked. opcache.preload_user is required when the master process starts as root. Two consequences to plan for: a file that cannot be compiled is skipped with a startup warning rather than stopping the server, and nothing you preload can be changed without restarting PHP-FPM, so preload library and application code, never configuration that a deploy rewrites.

Example · php
<?php

declare(strict_types=1);

// Referenced from php.ini as:
//   opcache.preload=/var/www/app/preload.php
//   opcache.preload_user=www-data

$files = new RegexIterator(
    new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator('/var/www/app/src')
    ),
    '/\.php$/'
);

foreach ($files as $file)
{
    opcache_compile_file($file->getPathname());
}

Clearing the cache as the last deploy step

The endpoint exists because a CLI reset cannot reach the web server's cache. hash_equals compares the token in constant time, and the route answers 404 rather than 403 so it does not advertise itself. opcache_get_status(false) returns the statistics without the per-script list, which is enough to log that the reset did something. A PHP-FPM reload achieves the same and needs no endpoint; use this when you cannot reload.

Example · php
<?php

declare(strict_types=1);

// public/deploy-clear.php - request this through the web server, so that the
// FPM workers are the processes clearing their own cache.
$sent = $_SERVER['HTTP_X_DEPLOY_TOKEN'] ?? '';
$expected = (string) getenv('DEPLOY_TOKEN');

if ($expected === '' || !hash_equals($expected, $sent))
{
    http_response_code(404);
    exit;
}

$status = opcache_get_status(false);
$cached = $status === false ? 0 : $status['opcache_statistics']['num_cached_scripts'];

opcache_reset();

header('Content-Type: text/plain; charset=utf-8');
echo "cleared {$cached} cached scripts\n";

Discussion

  • Be the first to comment on this lesson.