OPcache internals — shared memory, file invalidation
Concept
OPcache (Opcode Cache) stores the compiled opcode of PHP scripts in shared memory, eliminating the parse → compile step on subsequent requests. Without OPcache: every request parses every PHP file from disk. With OPcache: compiled opcodes are shared across all PHP-FPM worker processes.
The compilation pipeline (from eng-09-001):
- Source PHP → Lexer → Tokens
- Tokens → Parser → AST
- AST → Compiler → Opcodes (zend_op_array)
- OPcache stores step 3's output in shared memory.
- Next request: skip steps 1-3, execute opcodes directly.
Shared memory segment: OPcache allocates a block of memory at PHP-FPM startup (controlled by opcache.memory_consumption, default: 128MB). All workers read from this block — no per-process copy.
Script identification: Each script is keyed by its real path. OPcache stores the compiled opcodes, the file's mtime (modification time), and a checksum.
Invalidation: OPcache checks opcache.validate_timestamps (default: on) and opcache.revalidate_freq (default: 2 seconds). If the file's mtime changed, the cached entry is discarded and the file is recompiled. In production: disable validate_timestamps for maximum performance — files never change in production anyway. Invalidate manually after deployments.
Production settings:
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0 ; disable in production
opcache.revalidate_freq=0
opcache.save_comments=0 ; strip phpdoc in productionDeployment invalidation: opcache_reset() clears all cached scripts (requires a PHP request — can't call from CLI directly). Laravel Octane restarts the server. Most deployments restart PHP-FPM: systemctl reload php8.3-fpm.
OPcache + preloading (PHP 7.4+): opcache.preload — load a set of files into shared memory at PHP-FPM startup. Files are pre-compiled AND the opcode is already in memory before any request arrives. Laravel's php artisan optimize generates a preload file.
Code Example
; /etc/php/8.3/fpm/conf.d/10-opcache.ini
; Development settings
opcache.enable=1
opcache.memory_consumption=128 ; MB
opcache.max_accelerated_files=10000
opcache.validate_timestamps=1 ; check for file changes (dev)
opcache.revalidate_freq=0 ; check on EVERY request (dev only)
opcache.enable_cli=0 ; usually off for CLI
; Production settings
; opcache.validate_timestamps=0 ; NEVER check timestamps → no disk I/O
; opcache.revalidate_freq=0 ; irrelevant when validate_timestamps=0
; opcache.memory_consumption=256
; opcache.max_accelerated_files=30000
; opcache.save_comments=0 ; strip DocBlocks (saves memory)
; Preloading (PHP 7.4+)
; opcache.preload=/var/www/app/bootstrap/cache/preload.php
; opcache.preload_user=www-data
; Introspection — what's cached?<?php
// Check OPcache status
$status = opcache_get_status();
echo $status['memory_usage']['used_memory']; // bytes used
echo $status['memory_usage']['free_memory']; // bytes free
echo $status['opcache_statistics']['num_cached_scripts']; // cached files
echo $status['opcache_statistics']['hits']; // cache hits
echo $status['opcache_statistics']['misses']; // cache misses
// OPcache hit ratio — should be > 99% in production
$stats = $status['opcache_statistics'];
$hitRate = $stats['hits'] / ($stats['hits'] + $stats['misses']) * 100;
echo number_format($hitRate, 2) . '% hit rate';
// Invalidate a single file (useful after deployment)
opcache_invalidate('/var/www/app/public/index.php', force: true);
// Clear all cached scripts (called from a web request, not CLI)
opcache_reset();
// Check if a specific file is cached
opcache_is_script_cached('/var/www/app/vendor/laravel/framework/src/Illuminate/Foundation/Application.php');
// Laravel — artisan optimize pre-compiles config, routes, views, and generates preload.php
// php artisan optimize → generates bootstrap/cache/*.php
// php artisan optimize:clear → removes cached files
// After deployment: reload php-fpm to pick up new preload.php
// sudo systemctl reload php8.3-fpm