WordPress 7 has outgrown PHP’s default OPcache limits. Here’s how we fixed it across a cPanel and CloudLinux fleet

A few weeks ago we had a customer site go down 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. Every single 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. Running the same code from the CLI works. Only the web SAPI was broken, and opcache_reset() fixed it instantly.

That combination points to one thing: corrupted shared memory inside OPcache. Method and class names in compiled scripts live in OPcache’s interned strings buffer, and 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 garbage 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 a window of seconds to hours the site is broken.

The investigation

WordPress 7.0 added an OPcache section to Tools → Site Health → Info → Server. On a perfectly 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. That’s not “a bit tight”, that’s a buffer that’s been full for a long time and is one recompilation away from forcing a restart.

The defaults that produce this are in every stock php.ini:

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. It turns out I’m not the first person to notice: 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 run both caches completely full on simple sites. As of this writing the issue is still open and untriaged. Hosting providers have to fix this themselves.

Two things about OPcache internals that matter for sizing:

  1. The interned strings buffer is carved out of memory_consumption. Setting 256 / 64 leaves 192 MB for opcodes, not 256.
  2. OPcache shared memory is mmap’d and lazily committed. A 256 MB segment only consumes RAM for the pages that are actually written. Raising the ceiling costs you very little on accounts that don’t need it. The interned strings hash table is initialised upfront, but that’s small.
    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. That’s good for isolation and it means the sizing question is “how big is the largest single site”, not “how big is the sum of all sites”.

What we’re changing

Across the fleet, for every PHP version that has OPcache enabled:

opcache.memory_consumption=256
opcache.interned_strings_buffer=64
opcache.max_accelerated_files=20000
opcache.max_wasted_percentage=10

Why 64 and not 32? Because we haven’t been able to measure real demand yet. A buffer reporting 100% usage tells you it’s clipped, not what the site actually wants. The plan is to set a value generous enough that nothing is clipped, measure actual usage across accounts for a day (more on that below), and then tighten if the data says 64 is wasteful. Memory-wise the difference between 32 and 64 is almost entirely unresident pages, so 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.

One important point before we get into the mechanics: PHP silently ignores INI directives for extensions that aren’t loaded. If you set opcache.* on a PHP version where OPcache isn’t installed or enabled, nothing happens. No warning, no fatal. That makes it safe to deploy these settings uniformly and let the per-version and per-user OPcache state decide whether they take effect.

Before you touch anything: audit

Every server has a different mix of PHP versions, so the first step is a read-only look at what’s actually in effect. This is the script we used. For alt-php it has to run as a real cPanel user, because the CloudLinux PHP Selector links extensions per user and root’s CLI never loads OPcache at all (the first version of our audit printed blank rows for alt-php and I briefly thought OPcache was off everywhere).

#!/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 this produced:

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

Every version, both stacks, 128 / 8. The only thing that has moved in a decade is max_accelerated_files, which PHP 8.5 raised to 10000 and CloudLinux ships at 10000 on all alt-php builds.

EasyApache 4 / MultiPHP (ea-php)

On cPanel, each ea-php version has its own tree under /opt/cpanel/ea-phpXX/root/etc/. The WHM MultiPHP INI Editor edits php.ini in that tree. You can set the OPcache values there, one version at a time, on every server. Don’t. It doesn’t scale and it’s easy to miss a version.

Instead, use the php.d/ drop-in directory. Files there load after php.ini, in alphabetical order, and the last definition of a directive wins. cPanel documents php.d/local.ini as the file for custom settings that will survive EasyApache updates, and “local” sorts after “10-opcache”, so it overrides the stock 10-opcache.ini.

One trap worth knowing about: on ea-php56 the stock file is called opcache.ini, not 10-opcache.ini, and it contains the zend_extension=opcache.so line. If you create your own file named opcache.ini you will overwrite it and silently disable OPcache on PHP 5.6. Stick with local.ini.

for d in /opt/cpanel/ea-php*; do
  [ -d "$d/root/etc/php.d" ] || continue
  cat >> "$d/root/etc/php.d/local.ini" << 'EOF'
; fusioned opcache tuning
opcache.memory_consumption=256
opcache.interned_strings_buffer=64
opcache.max_accelerated_files=20000
opcache.max_wasted_percentage=10
EOF
done

A note on the MultiPHP INI Editor afterwards: its Basic Mode reads values from php.ini, so it will keep showing 128 and 8 even though the effective values are 256 and 64. Trust php -i, not the editor.

CloudLinux PHP Selector (alt-php)

alt-php is different in two ways. First, the extension set is per user: OPcache is only loaded for accounts where it’s ticked in the Selector. Second, there’s a single global override file that CloudLinux merges into every alt-php version:

/etc/cl.selector/global_php.ini

Directives under [Global] are applied to all alt-php builds when you run cagefsctl --setup-cl-selector. The opcache.* directives aren’t among the options users can change in the Selector UI, so nothing at the user level will override them.

[Global]
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 64
opcache.max_accelerated_files = 20000
opcache.max_wasted_percentage = 10

Then:

cagefsctl --setup-cl-selector

For users who don’t have OPcache enabled, the directives are inert (see the “silently ignored” point above). For users who do, they take effect on the next PHP process start.

Restart properly

Neither of the above restarts anything. On LiteSpeed, the OPcache shared memory segment belongs to the per-account lsphp parent process, and a graceful reload leaves existing parents running with the old segment size. You need a full restart:

/scripts/restartsrv_httpd

On Apache with PHP-FPM, restart the FPM pools for each version instead (/scripts/restartsrv_apache_php_fpm).

Verify per version afterwards:

/opt/cpanel/ea-php83/root/usr/bin/php -i | grep -E 'interned|memory_consumption'
su -s /bin/bash someuser -c '/opt/alt/php83/usr/bin/php -i' | grep -E 'interned|memory_consumption'

and check a WordPress Site Health page for the new numbers.

Conclusion

This is one of those problems that hides well. Nothing in the usual monitoring flags it, the outages look random, and the error message actively misleads you into debugging a plugin. The fix, once you know what you’re looking at, is four lines of INI in two files and a restart.

Three things I’d take away from it:

The default is not a recommendation. opcache.interned_strings_buffer=8 has 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 own customers’ sites. WordPress 7 put the numbers right in front of you. If you see 100.00% of 8 MB, you have a latent outage waiting for the next plugin update to trigger it.

Deploy uniformly, let PHP sort it out. Unknown directives are ignored, so you don’t need to track which versions or which accounts have OPcache enabled. Write the settings everywhere once and move on.

We’ve raised the limits to 256 / 64 / 20000 across the fleet and are collecting real usage numbers from opcache_get_status() before deciding whether 64 is the right long-term default or generous. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *