OPcache & Performance
How PHP Runs Your Code
In simple terms: Every time a PHP page is requested, PHP must read the source files, understand them, and translate them into instructions the engine can execute. Doing this translation again and again for every request is wasted work, because the code rarely changes. OPcache remembers the translated instructions in memory, so PHP can skip straight to running them — often making applications several times faster with no code changes at all. Technically: Executing a PHP file involves lexing, parsing into an abstract syntax tree, compiling into opcodes (bytecode for the Zend Engine), and executing the opcodes. OPcache, bundled with PHP, stores compiled opcodes in shared memory, so all PHP-FPM workers reuse them across requests. Additional features include preloading (loading code into memory at server start) and a JIT compiler (PHP 8.0+) that compiles hot code paths to machine code.
Without OPcache: source → parse → compile → execute (every request)
With OPcache: source → parse → compile → cache (first request)
cache ─────────────────────→ execute (later requests)
Enabling and Configuring OPcache
OPcache is a Zend extension, usually enabled by default in distribution packages and official Docker images (docker-php-ext-install opcache). Recommended production settings:
; conf.d/10-opcache.ini (production)
zend_extension = opcache
opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
opcache.save_comments = 1
| Directive | Meaning |
|---|---|
opcache.enable | Enable OPcache for web requests |
opcache.memory_consumption | Shared memory for opcodes, in MB — must hold your whole application and libraries |
opcache.interned_strings_buffer | Memory (MB) for de-duplicated strings such as class and function names |
opcache.max_accelerated_files | Maximum number of cached files; set above the number of PHP files in the project including vendor/ |
opcache.validate_timestamps | 1 = check files for changes (development); 0 = never check (fastest, production) |
opcache.revalidate_freq | With timestamps enabled: seconds between checks (0 = every request, good for development) |
opcache.save_comments | Keep doc comments — required by libraries that read annotations |
validate_timestamps = 0 Requires a Reset on Deploy
With timestamp validation disabled, PHP never notices changed files — deploying new code has no effect until the cache is cleared. Clear it as part of every deployment:
sudo systemctl reload php8.4-fpm(graceful reload; empties the cache),- or call
opcache_reset()through a protected internal endpoint or a tool such ascachetool. Deployments that switch a symlink to a new release directory (releases/2026-10-17→current) work well with this approach.
Inspecting the Cache
<?php
// admin/opcache-status.php — protect with authentication; run via the web (FPM), not the CLI
$status = opcache_get_status(false);
if ($status === false) {
exit("OPcache is not enabled for this SAPI\n");
}
$memory = $status['memory_usage'];
$stats = $status['opcache_statistics'];
printf("Cached scripts: %d\n", $stats['num_cached_scripts']);
printf("Hit rate: %.2f%%\n", $stats['opcache_hit_rate']);
printf("Memory used: %.1f MB of %.1f MB\n",
$memory['used_memory'] / 1048576,
($memory['used_memory'] + $memory['free_memory'] + $memory['wasted_memory']) / 1048576
);
printf("Wasted memory: %.1f%%\n", $memory['current_wasted_percentage']);
Output (example from a production server)
Cached scripts: 4182
Hit rate: 99.97%
Memory used: 118.4 MB of 256.0 MB
Wasted memory: 0.4%
Reading the Numbers
- Hit rate close to 100% means nearly every request uses cached opcodes.
- If free memory approaches zero or the number of cached scripts reaches
max_accelerated_files, files stop being cached — increase the limits. - Wasted memory grows when cached files change (with timestamp validation enabled); OPcache restarts automatically when it exceeds
opcache.max_wasted_percentage.
Preloading (PHP 7.4+)
Preloading loads selected files into OPcache when PHP-FPM starts, linking classes once for all requests:
opcache.preload = /var/www/shop/config/preload.php
opcache.preload_user = www-data
<?php
// config/preload.php
require __DIR__ . '/../vendor/autoload.php';
foreach (['Symfony\Component\HttpFoundation\Request', 'App\Kernel'] as $class) {
class_exists($class); // triggers autoloading, so the class is preloaded
}
Frameworks such as Symfony generate preload files automatically. Gains are typically modest (a few percent); after changing preloaded code, PHP-FPM must be restarted.
The JIT Compiler (PHP 8.0+)
The JIT (Just-In-Time) compiler translates frequently executed opcodes into native machine code:
opcache.jit = tracing
opcache.jit_buffer_size = 64M
Since PHP 8.4, the JIT is disabled by default through opcache.jit = disable and enabled by choosing a mode such as tracing.
| Workload | JIT Benefit |
|---|---|
| CPU-heavy code (math, image processing, algorithms, long-running workers) | Can be significant |
| Typical web applications (waiting for databases and network) | Usually small |
Measure your own application before and after enabling it.
Beyond OPcache: Where Time Really Goes
For most web applications, the biggest costs are I/O — database queries, external APIs, file access — not PHP execution. A performance checklist:
| Area | Technique | See |
|---|---|---|
| Database | Indexes, avoiding N+1 queries, selecting only needed columns, pagination | MySQL Joins with PHP, Pagination with PHP |
| Caching | Cache expensive results in APCu or Redis; HTTP caching with Cache-Control and ETags | HTTP Headers and Redirects |
| Autoloading | Optimized Composer class maps | Composer |
| Background work | Move emails, image processing, and API calls to queues | Sending Email |
| PHP-FPM | Enough workers for the load, without exhausting memory | PHP-FPM |
| Frontend | Compress responses, cache static assets, use a CDN | — |
| Current PHP version | Each PHP release brings engine optimizations | Migrating to PHP 8 |
Measure, Don't Guess
Use a profiler (Xdebug profiler, SPX, Blackfire, Tideways) on a realistic request to see where time is spent, optimize the largest cost, and measure again. Optimizing code that takes 1% of the request time cannot make the page noticeably faster.
Common Mistakes
Running Production Without OPcache
Effect: Every request recompiles all files — much slower and more CPU-intensive. Fix: Enable OPcache with adequate memory and file limits.
Disabling Timestamp Validation Without Resetting on Deploy
Effect: Old code keeps running after deployment. Fix: Reload PHP-FPM or reset OPcache as a deployment step.
Too Little Memory or Too Few Files
Effect: Parts of the application are not cached. Fix: Monitor opcache_get_status() and raise the limits.
Expecting the JIT to Fix Slow Pages
Effect: No improvement when the bottleneck is the database. Fix: Profile first; optimize queries and add caching.
Best Practices
- Enable OPcache in production with enough memory and
max_accelerated_filesfor the whole code base. - Disable timestamp validation in production and clear the cache on every deployment; enable it in development.
- Monitor the hit rate and memory usage.
- Consider preloading for frameworks that support it, and the JIT for CPU-heavy workloads.
- Profile real requests and fix the largest bottlenecks — usually database and I/O — first. Official reference: https://www.php.net/manual/en/book.opcache.php