0

OPcache — PHP's bytecode cache: compile once, serve from memory

Intermediate5 min read·eng-20-005
interviewperformance

Concept

OPcache — PHP's built-in bytecode cache. PHP normally parses and compiles every .php file on EVERY request. OPcache stores the compiled bytecode in shared memory, skipping the parse/compile step on subsequent requests.

What PHP normally does without OPcache:

  1. Read source file from disk.
  2. Parse PHP code into an AST (Abstract Syntax Tree).
  3. Compile AST to bytecode (opcodes).
  4. Execute the bytecode. Steps 1-3 are WASTED on every request if the file hasn't changed.

What OPcache does:

  1. First request: parse, compile, STORE bytecode in shared memory.
  2. Subsequent requests: skip to step 4 — execute cached bytecode directly.
  3. On file change: OPcache eventually detects the change (based on opcache.revalidate_freq) and recompiles.

Performance impact: OPcache typically delivers 2-4x performance improvement with no code changes. Essential for any production PHP application.

Configuration (php.ini):

  • opcache.enable=1: Enable OPcache (default on in PHP 7+).
  • opcache.memory_consumption=256: RAM for bytecode (MB). Size to hold all compiled files.
  • opcache.max_accelerated_files=20000: Number of files to cache.
  • opcache.validate_timestamps=0: In PRODUCTION, disable file change checks (faster but requires restart on deploy).
  • opcache.revalidate_freq=2: How often to check if file changed (seconds). Irrelevant if validate_timestamps=0.

Laravel optimization + OPcache: php artisan optimize caches config, routes, and views into single files. OPcache then caches those compiled files — doubly efficient.

Deployment with OPcache: If validate_timestamps=0, PHP won't detect new files after deploy. Solution: run opcache_reset() or send SIGUSR2 to PHP-FPM after deployment.

Code Example

ini
; php.ini / 99-opcache.ini — production OPcache settings

[opcache]
opcache.enable=1
opcache.enable_cli=0          ; no benefit for CLI scripts (artisan commands)
opcache.memory_consumption=256 ; 256MB of shared memory for bytecode
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000 ; adjust based on php artisan opcache:status
opcache.validate_timestamps=0  ; PRODUCTION: disable for max performance (restart on deploy!)
opcache.save_comments=1        ; needed for doctrine annotations / PHP 8 attributes
opcache.fast_shutdown=1
bash
# Check OPcache status
php -r "print_r(opcache_get_status());"

# Or use a web endpoint (restrict to internal only!)
# php artisan opcache:status  (with a package)

# DEPLOYMENT: reset OPcache after new code is deployed
# Option 1: Reload PHP-FPM (graceful — drains existing requests)
kill -USR2 $(cat /var/run/php-fpm.pid)
# or:
service php8.4-fpm reload

# Option 2: Reset via web endpoint (if validate_timestamps=0)
# /opcache-reset.php (delete after use, secure behind auth):
<?php
if (function_exists('opcache_reset')) {
    opcache_reset();
    echo 'OPcache reset';
}
php
<?php
// LARAVEL OPTIMIZE + OPCACHE = maximum performance
// php artisan optimize caches into single files:
// - bootstrap/cache/config.php  (all config merged)
// - bootstrap/cache/routes.php  (all routes compiled)
// - storage/framework/views/    (all Blade templates compiled)

// OPcache caches these compiled files too:
// - config.php: one file, compiled once → OPcache hit every request
// - Blade templates: compiled PHP → OPcache caches them too

// Without optimize: Laravel reads/parses 100+ config files per boot
// With optimize + OPcache: reads one cached file, bytecode already compiled

// Check what's in the cache:
$status = opcache_get_status();
echo "Used memory: " . ($status['memory_usage']['used_memory'] / 1024 / 1024) . " MB\n";
echo "Cached files: " . $status['opcache_statistics']['num_cached_scripts'] . "\n";
echo "Hit rate: " . $status['opcache_statistics']['opcache_hit_rate'] . "%\n";