OPcache — what it is and how to configure it
Concept
OPcache is the most impactful performance optimization available to any PHP application, and it costs nothing — it's bundled with PHP since 5.5. Understanding what it does, how to configure it, and how to invalidate it correctly separates senior engineers from developers who just "turn it on."
What OPcache actually does
Every PHP request normally goes through four stages: lex (source → tokens), parse (tokens → AST), compile (AST → opcodes), and execute (Zend VM runs opcodes). For a Laravel application that loads 300+ PHP files per request, this compilation work happens on every request — unless OPcache intercepts.
OPcache stores compiled opcodes (the bytecode) in shared memory — a region of RAM shared between all PHP-FPM worker processes. On the second request for app/Http/Controllers/UserController.php, OPcache returns the cached opcodes directly, skipping lex/parse/compile entirely. The result: 3–10x throughput improvement with no code changes.
Key configuration directives
; Enable OPcache
opcache.enable = 1
opcache.enable_cli = 0 ; Keep off for CLI unless you need it
; Memory
opcache.memory_consumption = 256 ; MB of shared memory for opcodes
opcache.interned_strings_buffer = 16 ; MB for interned strings (class names, etc.)
opcache.max_accelerated_files = 20000 ; Max files in cache (use: find . -name "*.php" | wc -l)
; Revalidation (production vs dev)
opcache.validate_timestamps = 0 ; PRODUCTION: never check disk (best performance)
opcache.validate_timestamps = 1 ; DEVELOPMENT: check disk on each request
opcache.revalidate_freq = 0 ; When validate_timestamps=1, check every N seconds (0=every request)
; Correctness & optimization
opcache.save_comments = 1 ; Required for Doctrine/Attributes, PHPDoc-based frameworks
opcache.enable_file_override = 0 ; Keep off unless you know exactly what it does
opcache.optimization_level = 0x7FFEBFFF ; All optimizations enabled (default is fine)Cache invalidation
With validate_timestamps = 0 in production, you must manually invalidate the cache after deployment. Options:
- Restart PHP-FPM:
systemctl reload php8.4-fpm(graceful reload, not hard restart) opcache_reset(): Call this from a web request or CLI. From CLI you needopcache.enable_cli=1and the CLI shares a different cache than FPM.opcache_invalidate($file, true): Invalidate a specific file.- Deploy scripts: Laravel Envoyer, Deployer, and GitHub Actions all have opcache invalidation steps.
The CLI and FPM processes use separate shared memory segments. Calling opcache_reset() from php artisan does NOT flush FPM's cache. You must call it via an HTTP request (a deploy endpoint) or restart FPM.
Preloading (PHP 7.4+)
opcache.preload = /path/to/preload.php runs a PHP script at FPM startup that loads selected files into shared memory permanently. Laravel's preload script can load all framework files once, reducing per-request opcache lookup overhead. This gives 5–15% additional throughput on top of normal OPcache.
opcache.preload = /var/www/app/preload.php
opcache.preload_user = www-data ; Required: must specify non-root userCode Example
<?php
declare(strict_types=1);
// OPcache status inspection — useful in a health check or monitoring endpoint
if (!function_exists('opcache_get_status')) {
echo "OPcache not available.\n";
exit(1);
}
$status = opcache_get_status(false); // false = don't include per-file stats
if ($status === false) {
echo "OPcache is disabled.\n";
exit(1);
}
$mem = $status['memory_usage'];
$totalMem = $mem['used_memory'] + $mem['free_memory'] + $mem['wasted_memory'];
$usedPct = round(($mem['used_memory'] / $totalMem) * 100, 1);
$wastedPct = round(($mem['wasted_memory'] / $totalMem) * 100, 1);
echo "OPcache Status:\n";
echo " Enabled: " . ($status['opcache_enabled'] ? 'yes' : 'no') . "\n";
echo " Memory used: {$usedPct}%\n";
echo " Memory wasted:{$wastedPct}% (high means cache is full — increase memory_consumption)\n";
echo " Cached files: " . $status['opcache_statistics']['num_cached_scripts'] . "\n";
echo " Cache hits: " . $status['opcache_statistics']['hits'] . "\n";
echo " Cache misses: " . $status['opcache_statistics']['misses'] . "\n";
$hitRate = $status['opcache_statistics']['opcache_hit_rate'];
echo " Hit rate: " . round($hitRate, 1) . "% (target: >99%)\n";
// If wasted_memory > 5%, consider calling opcache_reset() or increasing memory
if ($wastedPct > 5) {
echo "\nWARNING: High wasted memory. Cache may be full. Consider:\n";
echo " 1. Increasing opcache.memory_consumption\n";
echo " 2. Increasing opcache.max_accelerated_files\n";
echo " 3. Running opcache_reset() after deployment\n";
}
// Safe way to call reset from a deploy script (must be via HTTP, not CLI)
// if (isset($_GET['reset_opcache']) && isFromTrustedNetwork()) {
// opcache_reset();
// }Interview Q&A
Q: What exactly is stored in OPcache's shared memory and why does it speed things up?
OPcache stores compiled opcodes — the bytecode representation of your PHP source files after lexing, parsing, and compiling. For a typical Laravel request that autoloads 200–400 PHP files, every file would normally go through lex/parse/compile on each request. With OPcache, only the first request incurs this cost; subsequent requests find the compiled opcodes in shared memory and skip directly to execution. The shared memory is accessible by all FPM worker processes without IPC overhead because it's a memory-mapped segment. OPcache also performs optimization passes on the opcodes (constant folding, dead code elimination) that make the final bytecode faster than what the compiler would produce without caching.
Q: Why does calling opcache_reset() from an Artisan command not flush the FPM cache?
PHP-FPM workers and CLI processes run in separate OS processes with separate virtual address spaces. When OPcache creates its shared memory segment, each process type gets its own segment (or maps the FPM segment independently). A CLI php process cannot reach into the memory segment that FPM workers are using. The solution is to call opcache_reset() via an HTTP endpoint (a deployment webhook handler in your app, protected by a secret token) or, preferably, to send a SIGUSR2 signal to PHP-FPM for a graceful reload: kill -USR2 $(cat /run/php/php8.4-fpm.pid). Laravel Envoyer and Deployer handle this automatically.
Q: What is the hit_rate metric in OPcache and what should it be in production?
The hit rate is the percentage of file inclusion requests that were served from the opcode cache vs had to be compiled fresh. In production with validate_timestamps = 0, a healthy application should maintain a hit rate above 99% after the cache warms up. A low hit rate means either the cache is too small (increase memory_consumption), you have too many unique files (increase max_accelerated_files), or the cache is being invalidated frequently (check your deploy process). During a rolling restart of FPM workers the hit rate temporarily drops to 0% as workers restart and re-warm. Monitoring this metric with Prometheus/Grafana or Datadog is standard practice for production PHP services.