petr_sys14 May 2026 04:52

Spent two days tuning OPcache after a deploy caused 40% latency spike. Sharing what changed and what the numbers look like.

You can check your current OPcache state at runtime with this script:

PHP
<?php
$status = opcache_get_status(false);
$config = opcache_get_configuration();
if (!$status) {
echo "OPcache is disabled\n";
exit;
}
$used = $status['memory_usage']['used_memory'];
$free = $status['memory_usage']['free_memory'];
$wasted = $status['memory_usage']['wasted_memory'];
$total = $used + $free + $wasted;
$scripts = $status['opcache_statistics']['num_cached_scripts'];
$hits = $status['opcache_statistics']['hits'];
$misses = $status['opcache_statistics']['misses'];
$ratio = $hits + $misses > 0 ? round($hits / ($hits + $misses) * 100, 2) : 0;
printf("Memory: %.1f MB used / %.1f MB total (%.1f%% wasted)\n",
$used / 1024 / 1024,
$total / 1024 / 1024,
$wasted / $total * 100
);
printf("Scripts: %d cached\n", $scripts);
printf("Hit ratio: %.2f%%\n", $ratio);
printf("Restarts (OOM): %d\n", $status['opcache_statistics']['oom_restarts']);
printf("Restarts (hash): %d\n", $status['opcache_statistics']['hash_restarts']);
// Warn if hit ratio is low or restarts are happening
if ($ratio < 99) {
echo "WARNING: hit ratio below 99%, check opcache.memory_consumption\n";
}
if ($status['opcache_statistics']['oom_restarts'] > 0) {
echo "WARNING: OOM restarts detected, increase opcache.memory_consumption\n";
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

The settings that made the biggest difference for us: opcache.memory_consumption=256 (was 128), opcache.max_accelerated_files=20000 (was 4000), and opcache.validate_timestamps=0 in production with a deploy hook that calls opcache_reset().

Run the script on your server to see current state.

Replies (2)
bohdan_v14 May 2026 05:20

The oom_restarts counter is the one to watch. Every restart throws away the entire cache and rebuilds it from scratch, which causes the latency spike you described. We had the same issue with a Laravel app that has about 8000 files including vendor.

One thing worth adding to your script: the keys saturation check. If num_cached_keys approaches max_cached_keys, you also get restarts.

PHP
<?php
$status = opcache_get_status(false);
if (!$status) {
echo "OPcache disabled\n";
exit;
}
$stats = $status['opcache_statistics'];
$cachedKeys = $stats['num_cached_keys'];
$maxKeys = $stats['max_cached_keys'];
$saturation = round($cachedKeys / $maxKeys * 100, 1);
printf("Keys: %d / %d (%.1f%% full)\n", $cachedKeys, $maxKeys, $saturation);
if ($saturation > 90) {
echo "WARNING: keys nearly full, increase opcache.max_accelerated_files\n";
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
0
nphp14 May 2026 07:45

For Swoole specifically: opcache_reset() in a coroutine context does not reset OPcache in other workers. Each worker process has its own OPcache. You need to broadcast the reset to all workers or use a rolling restart.

We use a SIGUSR1 signal handler in each worker that calls opcache_reset() on receive, then the deploy script sends kill -SIGUSR1 to all worker PIDs. Works cleanly without dropping connections.

0
Write a reply
Markdown. ```php blocks are runnable.