PHP OPcache Misconfiguration — Why Your WordPress Site Slows Down After Updates

· 11 min read

A client's WooCommerce site was behaving strangely. After updating a shipping plugin, the old shipping rates were still appearing at checkout. Clearing the WordPress object cache and page cache made no difference. The browser cache was ruled out. The plugin code on disk was correct — I verified the updated files were there. Yet PHP was executing the old version.

The culprit was OPcache — PHP's built-in bytecode cache — serving stale compiled code because it had been misconfigured since the server was first provisioned.

What OPcache Does and Why It Matters

Every time PHP handles a request, it reads your .php files from disk, compiles them into bytecode, executes the bytecode, then throws it away. OPcache stores that compiled bytecode in shared memory so subsequent requests skip the compilation step entirely. On a typical WordPress site, this cuts response times by 30-50%.

The problem is that OPcache has several settings that interact in non-obvious ways. Get them wrong, and you end up with one of three scenarios: stale code served after updates, constant cache flushes that kill performance, or silent memory pressure that degrades the entire server.

Diagnosing the Problem

The first step is checking whether OPcache is even running and how it's configured. SSH into the server and create a temporary diagnostic script:

<?php
// /tmp/opcache-check.php — delete after use
$status = opcache_get_status(false);
$config = opcache_get_configuration();

echo "=== OPcache Status ===\n";
echo "Enabled: " . ($status['opcache_enabled'] ? 'Yes' : 'No') . "\n";
echo "Hit rate: " . round($status['opcache_statistics']['opcache_hit_rate'], 2) . "%\n";
echo "Used memory: " . round($status['memory_usage']['used_memory'] / 1048576, 1) . " MB\n";
echo "Free memory: " . round($status['memory_usage']['free_memory'] / 1048576, 1) . " MB\n";
echo "Wasted memory: " . round($status['memory_usage']['wasted_memory'] / 1048576, 1) . " MB\n";
echo "Cached files: " . $status['opcache_statistics']['num_cached_scripts'] . "\n";
echo "Max cached keys: " . $status['opcache_statistics']['max_cached_keys'] . "\n";
echo "OOM restarts: " . $status['opcache_statistics']['oom_restarts'] . "\n";
echo "Hash restarts: " . $status['opcache_statistics']['hash_restarts'] . "\n";
echo "Manual restarts: " . $status['opcache_statistics']['manual_restarts'] . "\n\n";

echo "=== Key Settings ===\n";
echo "memory_consumption: " . $config['directives']['opcache.memory_consumption'] . " MB\n";
echo "max_accelerated_files: " . $config['directives']['opcache.max_accelerated_files'] . "\n";
echo "validate_timestamps: " . $config['directives']['opcache.validate_timestamps'] . "\n";
echo "revalidate_freq: " . $config['directives']['opcache.revalidate_freq'] . " seconds\n";
echo "interned_strings_buffer: " . $config['directives']['opcache.interned_strings_buffer'] . " MB\n";

Run it via PHP-FPM (not CLI — the CLI has its own OPcache instance):

curl -s http://localhost/tmp/opcache-check.php

Or if that's not accessible, run it through WP-CLI's eval command, which uses the same PHP configuration:

wp eval 'include "/tmp/opcache-check.php";' --path=/var/www/html

On the client's server, the output immediately revealed the problems:

Hit rate: 87.3%
Used memory: 63.8 MB
Free memory: 0.2 MB
Wasted memory: 0.0 MB
Cached files: 4,847
Max cached keys: 4,999
OOM restarts: 23
Hash restarts: 14

memory_consumption: 64 MB
max_accelerated_files: 4000
validate_timestamps: 0
revalidate_freq: 2
interned_strings_buffer: 8 MB

Three red flags at once.

Problem 1: validate_timestamps Was Disabled

With validate_timestamps=0, OPcache never checks whether the source file on disk has changed. It compiles the file once and serves that bytecode forever — or until OPcache is manually reset, PHP-FPM is restarted, or the cache runs out of memory and flushes.

This is a valid setting for deployment pipelines where you explicitly call opcache_reset() after each deploy. But on a WordPress site where updates happen through the admin dashboard, it means plugin and theme updates appear to have no effect. WordPress writes the new files to disk, but PHP keeps executing the old bytecode.

This was why the client's shipping plugin appeared stuck on the old version. The updated PHP files were sitting on disk, completely ignored.

The fix:

; /etc/php/8.2/conf.d/10-opcache.ini
opcache.validate_timestamps=1
opcache.revalidate_freq=60

Setting revalidate_freq=60 means OPcache checks each file's modification time at most once every 60 seconds. This is a sensible balance — updates take effect within a minute, but you're not hammering the filesystem with stat() calls on every request.

For staging or development servers, set revalidate_freq=0 so changes are picked up immediately.

Problem 2: Memory Was Exhausted

The cache had 64MB allocated and was using 63.8MB with 23 OOM (out-of-memory) restarts. Every time OPcache fills up, it dumps the entire cache and starts over. Every request after a flush recompiles every PHP file it touches from scratch — a cold start that can spike response times from 200ms to 2-3 seconds.

With 23 OOM restarts, this server had been periodically flushing and rebuilding its cache dozens of times, causing intermittent slowdowns that were nearly impossible to correlate with any specific action.

To size OPcache memory correctly, count your PHP files:

find /var/www -name "*.php" | wc -l

On this server, the answer was 14,200 files. A WooCommerce site with a page builder and 20+ plugins generates a lot of PHP. As a rough guide, budget 2-3KB of OPcache memory per PHP file:

14,200 files x 3 KB = ~42 MB minimum
Add 50% headroom  = ~64 MB ... barely enough
Add interned strings and overhead = needs 128-256 MB

The 64MB allocation was theoretically sufficient for the raw bytecode, but left no room for interned strings overhead, and any plugin update that added new files would push it over the edge.

opcache.memory_consumption=256
opcache.interned_strings_buffer=32

Problem 3: File Limit Was Too Low

The max_accelerated_files was set to 4,000 (internally rounded to 4,999 — OPcache uses the next prime number). With 14,200 PHP files on the site, only a third of them could be cached at any time. The 14 hash restarts confirmed the index was constantly overflowing.

WordPress core alone has around 1,500 PHP files. Add WooCommerce (1,200+), a page builder like Elementor (2,000+), and a stack of plugins, and you easily exceed 10,000. Check your actual count and add a 30% buffer:

find /var/www -name "*.php" | wc -l
# 14,200

# Set to at least 18,500 — OPcache rounds up to next prime
opcache.max_accelerated_files=20000

The Complete Fix

Here's the corrected OPcache configuration I applied:

; /etc/php/8.2/conf.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=60
opcache.save_comments=1
opcache.max_wasted_percentage=10

After restarting PHP-FPM:

sudo systemctl restart php8.2-fpm

I ran the diagnostic script again a few hours later:

Hit rate: 99.8%
Used memory: 112.4 MB
Free memory: 143.6 MB
Cached files: 14,187
Max cached keys: 32,531
OOM restarts: 0
Hash restarts: 0

The hit rate jumped from 87% to 99.8%. Response times dropped by 40% across the board, and the intermittent slowdowns stopped completely.

A Note on JIT

PHP 8.1+ includes a JIT (Just-In-Time) compiler built into OPcache. For WordPress and WooCommerce, JIT provides minimal benefit — WordPress workloads are I/O-bound (database queries, HTTP requests), not CPU-bound. Worse, JIT has caused segfaults in certain PHP 8.3 builds when interacting with specific WordPress plugins.

Unless you have a specific CPU-intensive use case, leave JIT disabled:

opcache.jit=off
opcache.jit_buffer_size=0

Recommended Settings by Server Type

Setting Small Blog WooCommerce Store Multi-Site / Agency
memory_consumption 128 256 512
interned_strings_buffer 16 32 64
max_accelerated_files 10000 20000 30000
validate_timestamps 1 1 1
revalidate_freq 60 60 60
save_comments 1 1 1

Always set save_comments=1 — some WordPress plugins and dependency injection containers rely on reading PHP docblock comments at runtime. Disabling this breaks things in ways that are extremely hard to debug.

How to Monitor OPcache Ongoing

Don't set and forget. I add a simple OPcache health check to the monitoring stack on every server I manage. A WP-CLI command you can run from cron or a monitoring script:

wp eval '
$s = opcache_get_status(false);
$hit = round($s["opcache_statistics"]["opcache_hit_rate"], 1);
$oom = $s["opcache_statistics"]["oom_restarts"];
$hash = $s["opcache_statistics"]["hash_restarts"];
if ($hit < 99 || $oom > 0 || $hash > 0) {
    echo "WARNING: OPcache hit={$hit}% oom_restarts={$oom} hash_restarts={$hash}\n";
    exit(1);
}
echo "OK: OPcache hit={$hit}%\n";
' --path=/var/www/html

If you're using Telegraf, the phpfpm input plugin combined with a custom exec script gives you historical OPcache metrics in Grafana — invaluable for spotting slow degradation over time.

Why This Keeps Happening

OPcache misconfiguration is so common because the defaults are conservative (designed for shared hosting where many sites share one OPcache pool), hosting control panels rarely expose OPcache settings in their UI, and most WordPress performance guides focus on page caching and CDNs while ignoring the PHP layer entirely.

This is part of what I check during server onboarding for my server management clients. A properly tuned OPcache is one of the highest-impact, lowest-effort performance improvements you can make — and getting it wrong creates problems that look like they're caused by something else entirely.

Need help with something similar? Check out my maintenance plans.

Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for UK agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans Get in Touch

Get in Touch to Discuss Your Needs