Every time PHP runs a script, it normally parses and compiles the source code into opcodes. OPcache stores those compiled opcodes in shared memory, so later requests skip that work. It’s bundled with PHP, typically gives a large speed-up for frameworks and CMSs like WordPress and Laravel, and is one of the easiest performance wins on any PHP server.

Check that it’s enabled

php -i | grep -E 'opcache.enable|opcache.memory_consumption'      # CLI settings
php-fpm8.3 -i | grep opcache.enable                                # FPM may differ

Or create a temporary page with <?php var_dump(opcache_get_status(false)); (then delete it). On Debian/Ubuntu, install with sudo apt install php8.3-opcache (usually installed already).

Create /etc/php/8.3/fpm/conf.d/99-opcache.ini:

opcache.enable=1
opcache.memory_consumption=256        ; MB of shared memory for compiled code
opcache.interned_strings_buffer=16    ; MB for strings (frameworks use many)
opcache.max_accelerated_files=20000   ; must exceed your PHP file count
opcache.validate_timestamps=1         ; check for changed files…
opcache.revalidate_freq=60            ; …at most every 60 s
opcache.save_comments=1               ; required by many frameworks (annotations)
opcache.enable_file_override=1
sudo systemctl reload php8.3-fpm

Count your PHP files to size max_accelerated_files:

find /var/www -name '*.php' | wc -l

See php.ini settings.

Deploys and stale code

With validate_timestamps=1 and revalidate_freq=60, changed files are picked up within a minute. That’s fine for most sites, but during a deploy, requests can briefly mix old and new code.

For maximum performance on servers where code only changes during deploys:

opcache.validate_timestamps=0

Then you must clear OPcache on every deploy, or PHP keeps serving the old code:

sudo systemctl reload php8.3-fpm        # simplest: graceful reload clears the cache

Symlink-based deploys (current -> releases/123) need care: reload PHP-FPM after switching the symlink, or use $realpath_root in Nginx’s SCRIPT_FILENAME, so new paths are compiled fresh. See Nginx with PHP-FPM and deploy Laravel with Nginx.

Is it big enough?

<?php
$s = opcache_get_status(false);
printf("memory used: %.1f MB, free: %.1f MB\n", $s['memory_usage']['used_memory']/1e6, $s['memory_usage']['free_memory']/1e6);
printf("cached scripts: %d, hit rate: %.2f%%\n", $s['opcache_statistics']['num_cached_scripts'], $s['opcache_statistics']['opcache_hit_rate']);
printf("restarts (oom): %d\n", $s['opcache_statistics']['oom_restarts']);
  • Hit rate should be above 99% on a warmed-up server.
  • oom_restarts above zero, or memory close to full, means: raise memory_consumption.
  • Many sites on one server share one OPcache per PHP-FPM master. Size it for all of them.

Preloading (PHP 7.4+)

opcache.preload loads chosen files into memory at FPM start, so they’re always compiled. Some frameworks generate preload scripts (Symfony does). Gains are modest for most apps, and code changes require an FPM restart.

JIT (PHP 8+)

opcache.jit_buffer_size=64M
opcache.jit=tracing

JIT helps CPU-heavy code (math, image processing), but typically gives little for web apps dominated by database and I/O time. Measure before and after.

CLI

opcache.enable_cli=0 (the default) is right for most cases. Command-line scripts are short-lived, and don’t benefit.

Security note

On shared servers with several sites, OPcache is shared within one FPM master. Use separate pools (and ideally separate users), and keep opcache.validate_permission=1 and opcache.validate_root=1 where users shouldn’t see each other’s cached code. See PHP-FPM tuning.

Quick reference

Setting Typical value
opcache.memory_consumption 128–512 MB
opcache.interned_strings_buffer 16–32 MB
opcache.max_accelerated_files > number of PHP files (e.g. 20000)
opcache.validate_timestamps 1 (or 0 + reload on deploy)
Clear cache systemctl reload php8.3-fpm
Health Hit rate > 99%, no OOM restarts

Related: WordPress TTFB and LEMP stack on Ubuntu.

  • PHP
  • Performance
  • Caching