WordPress 7 has outgrown PHP's default OPcache limits. Here's how we fixed it across a cPanel and CloudLinux fleet
Random WordPress outages led us to an exhausted OPcache buffer and PHP defaults that haven't kept pace. Here's how we investigated and raised the limits across our cPanel and CloudLinux fleet.
Updated October 2026: cPanel removes php.d/local.ini on every update, so the ea-php section now uses a zz- drop-in file, and the CageFS and restart steps have changed.
A few weeks ago we had a customer site go down for six hours with a fatal error I had never seen before. Since then I've found the same root cause behind a handful of "random" outages across our fleet, traced it to a PHP default that hasn't changed in over a decade, and rolled out a fix on every cPanel server we run. This is the write-up I wish had existed when I started digging.
If you run WordPress on cPanel, with or without CloudLinux, you almost certainly have the same problem. It just hasn't bitten you yet.
The symptom
The outage that started this was a WordPress site with WPML, Elementor, Yoast and Wordfence. It began returning HTTP 500 at 05:05 UTC and stayed down until 11:06. Every request logged the same fatal:
PHP Fatal error: Uncaught Error: Call to undefined method WPML\Container\Container::()
Note the method name. There isn't one. The class exists, the file on disk is intact, and php -l is happy. The same code runs fine from the CLI. Only the web SAPI was broken, and opcache_reset() fixed it instantly.
That combination points to corrupted shared memory inside OPcache. Method and class names in compiled scripts live in OPcache's interned strings buffer. When that buffer is exhausted and OPcache has to restart itself under load, you can end up with a cached script whose string references point at nothing. The site doesn't crash cleanly; it serves errors until something flushes the cache.
Once I knew what to look for, I went back through the last couple of months of tickets. Several "site was down for 20 minutes, came back on its own" incidents fit the same shape: a plugin or core update recompiles a lot of files at once, the interned strings buffer fills, OPcache schedules a restart, and for anything from seconds to hours the site is broken.
The investigation
WordPress 7.0 added an OPcache section to Tools → Site Health → Info → Server. On an ordinary WP 7 site with a normal plugin stack it shows this:
Opcode cache Enabled
Opcode cache memory usage 75 MB of 128 MB
Opcode cache interned strings usage 100.00% of 8 MB (24 B free)
Opcode cache hit rate 95.93%
24 bytes free. The buffer had been full for a long time and was one recompilation away from forcing a restart.
These are the stock php.ini defaults:
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000 ; 4000 before PHP 8.5
The 8 MB interned strings default was sized for the PHP ecosystem of 2013. WordPress core alone has roughly doubled in size since then, and a typical site loads a page builder, an SEO plugin, a security plugin and WooCommerce on top. Ollie Jones, who wrote the Site Health check, opened php-src #21423 in March 2026 asking the PHP project to double memory_consumption and triple interned_strings_buffer, because WordPress 6.9 and 7.0 fill both caches on simple sites. As of this writing the issue is still open and untriaged, so hosting providers have to fix this themselves.
Two things about OPcache internals matter for sizing:
- The interned strings buffer comes out of
memory_consumption. Setting 256 / 64 leaves 192 MB for opcodes, not 256. - OPcache shared memory is mmap'd and lazily committed. A 256 MB segment only uses RAM for the pages that get written, so raising the ceiling costs very little on accounts that don't need it.
On LiteSpeed with CloudLinux (our setup), each cPanel account's lsphp process tree has its own OPcache segment, so these limits are per account, not per server. The sizing question is "how big is the largest single site", not "how big is the sum of all sites".
What we're changing
For every PHP version on every server:
opcache.memory_consumption=256
opcache.interned_strings_buffer=64
opcache.max_accelerated_files=20000
opcache.max_wasted_percentage=10
Why 64 and not 32? A buffer at 100% tells you it's clipped, not how much the site wants. We set a value high enough that nothing is clipped, then measure real usage and tighten if 64 turns out to be wasteful. Because of lazy commit, starting high is cheap.
max_accelerated_files is rounded up internally to the next prime from a fixed list, so 20000 becomes 32531. The hash table cost is negligible.
PHP ignores INI directives for extensions that aren't loaded, with no warning. That makes it safe to deploy these settings everywhere and let the per-version and per-user OPcache state decide where they apply.
Before you touch anything: audit
Each server has its own mix of PHP versions, so start with a read-only check. For alt-php the script runs PHP as a cPanel user, because the CloudLinux PHP Selector loads extensions per user and root's CLI never loads OPcache.
#!/bin/bash
# opcache-audit.sh - read-only. Usage: ./opcache-audit.sh [-u cpanel_user]
set -u
AUDIT_USER=""
while getopts "u:" o; do [ "$o" = u ] && AUDIT_USER=$OPTARG; done
q() {
local cmd='echo ini_get("opcache.memory_consumption")," ",ini_get("opcache.interned_strings_buffer")," ",ini_get("opcache.max_accelerated_files"),"\n";'
if [ -n "${2:-}" ]; then
su -s /bin/bash "$2" -c "$1 -d display_errors=0 -r '$cmd'" 2>/dev/null
else
"$1" -d display_errors=0 -r "$cmd" 2>/dev/null
fi
}
printf "%-10s %-6s %-6s %-6s\n" VERSION MEM INTERN FILES
for d in /opt/cpanel/ea-php*; do
bin="$d/root/usr/bin/php"; [ -x "$bin" ] || continue
read mem int files <<< "$(q "$bin")"
printf "%-10s %-6s %-6s %-6s\n" "$(basename "$d")" "${mem:--}" "${int:--}" "${files:--}"
done
if [ -d /etc/cl.selector ]; then
[ -z "$AUDIT_USER" ] && AUDIT_USER=$(grep -rls '^opcache' /home/*/.cl.selector/alt_php.ini 2>/dev/null | head -1 | cut -d/ -f3)
echo; echo "alt-php (as user: ${AUDIT_USER:-NONE})"
for d in /opt/alt/php[0-9]*; do
bin="$d/usr/bin/php"; [ -x "$bin" ] || continue
read mem int files <<< "$(q "$bin" "$AUDIT_USER")"
printf "%-10s %-6s %-6s %-6s\n" "alt-$(basename "$d")" "${mem:--}" "${int:--}" "${files:--}"
done
fi
On one of our servers:
VERSION MEM INTERN FILES
ea-php56 128 8 4000
ea-php74 128 8 4000
ea-php81 128 8 4000
ea-php82 128 8 4000
ea-php83 128 8 4000
ea-php84 128 8 4000
ea-php85 128 8 10000
alt-php (as user: someuser)
alt-php82 128 8 10000
alt-php83 128 8 10000
alt-php84 128 8 10000
alt-php85 128 8 10000
All versions on both stacks were at 128 / 8. Only max_accelerated_files differed: PHP 8.5 raised its default to 10000, and CloudLinux ships 10000 on all alt-php builds.
EasyApache 4 / MultiPHP (ea-php)
Each ea-php version has its own config tree under /opt/cpanel/ea-phpXX/root/etc/. You can set the values per version in the WHM MultiPHP INI Editor, but across many versions and servers that doesn't scale. Use the php.d/ directory instead: PHP loads its files after php.ini, in alphabetical order, and the last value wins.
The first version of this post used php.d/local.ini. Don't. cPanel has deprecated that filename, and migrate_local_ini_to_php_ini runs on every upcp, including the nightly upcp --security, and removes it. We lost the file on two servers within days, and both went back to 128 / 8 without any error.
Use your own file, for example zz-custom-opcache.ini. It sorts after 10-opcache.ini, and cPanel's updates leave it alone. Three things to watch:
- ea-php56 ships its OPcache config as
opcache.ini, with thezend_extensionline in it. Never write a file with that filename, or OPcache is off on 5.6. - ea-php85 has no OPcache ini at all, since OPcache is built into PHP 8.5. Loop over the PHP binaries, not over existing
*opcache.inifiles, or you skip 8.5. - zzzzzzz-pecl.ini loads after
zz-. Make sure it sets noopcache.*values.
for bin in /opt/cpanel/ea-php*/root/usr/bin/php; do
[ -x "$bin" ] || continue
dir="${bin%/usr/bin/php}/etc/php.d"
[ -d "$dir" ] || continue
cat > "$dir/zz-custom-opcache.ini" << 'EOF'
opcache.memory_consumption=256
opcache.interned_strings_buffer=64
opcache.max_accelerated_files=20000
opcache.max_wasted_percentage=10
EOF
done
command -v cagefsctl >/dev/null && cagefsctl --force-update
The last line matters on CloudLinux. With CageFS, lsphp reads a copy of the ea-php config inside the cage, and the new file doesn't reach the websites until cagefsctl --force-update runs. Test for cagefsctl, not for PHP Selector: one of our servers runs CageFS without the Selector, and our first rollout skipped it.
The MultiPHP INI Editor reads php.ini, so it will keep showing 128 and 8 afterwards. That's expected.
CloudLinux PHP Selector (alt-php)
For alt-php, CloudLinux merges one global override file into every version: /etc/cl.selector/global_php.ini. Users can't change opcache.* in the Selector UI, so nothing at the user level overrides it.
[Global PHP Settings]
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 64
opcache.max_accelerated_files = 20000
opcache.max_wasted_percentage = 10
cagefsctl --setup-cl-selector
Accounts without OPcache enabled in the Selector ignore these values.
Restart properly
OPcache memory belongs to the PHP processes, so nothing changes until they are replaced. Restart the web server through cPanel, which works for both Apache and LiteSpeed:
/scripts/restartsrv_httpd
If you run PHP-FPM, restart the pools too with /scripts/restartsrv_apache_php_fpm.
On LiteSpeed, detached lsphp workers survive a web server restart and keep their old OPcache segment. Kill them with pkill lsphp, or per account with pkill -u someuser lsphp (keep lsphp in there, or you kill everything the user runs). Every site starts with a cold cache afterwards, so do this at a quiet hour, one server at a time.
Verify from the web side, with WordPress Site Health or a phpinfo() page. On CageFS servers, php -i as root reads the files outside the cage and can show the new values while the sites still run the old ones.
Conclusion
This problem hides well. Nothing in the usual monitoring flags it, the outages look random, and the error message sends you off debugging a plugin. The fix is four lines of INI, a CageFS update and a proper worker restart.
What I'd take away from it:
- The default is not a recommendation.
opcache.interned_strings_buffer=8has survived unchanged since PHP 5.5 while the code it caches has grown several times over. If your stack looks anything like WordPress 7 plus a page builder, assume the defaults are wrong until Site Health proves otherwise. - Look at Site Health on a few of your customers' sites. If you see
100.00% of 8 MB, you have an outage waiting for the next plugin update. - Deploy everywhere and let PHP sort it out. Unknown directives are ignored, so you don't need to track which versions or accounts have OPcache enabled.
- Check your config after the next cPanel update. Our first rollout disappeared within days, and nothing told us.
We've raised the limits to 256 / 64 / 20000 across the fleet and are collecting usage numbers from opcache_get_status() before deciding whether 64 is the right long-term default. I'll follow up with the data.
If you've seen the same blank-method fatals on your servers, or you have interned strings numbers from a large fleet of your own, I'd like to hear about it.