80 WordPress sites, one OPcache
Hosting our clients' websites means taking on technical challenges every day. Today I'd like to share one of them, which…
ReadIn part one, an OPcache patch stored the identical bytecode of 80 WordPress sites only once, and nearly halved the cache's memory over a one-million-request bench. Since October 29, it has been running in production on our shared hosting server. Here is how it went, and what the graphs taught us along the way, much of which has nothing to do with the patch.
Case study · PHP 8.4 · OPcache
This article follows up on 80 WordPress sites, one OPcache
Vitalyn · November 2026 · PHP 8.4.27, opcache-dedup patch
| Metric | Result | Detail |
|---|---|---|
| OPcache memory | -43% | 5.4 GB of bytecode fits in 3.1 GB |
| Shared functions | 82.5% | 1.9 million out of 2.3 million point to an already stored block |
| Worker crashes | 0 | no SIGSEGV, SIGBUS or SIGABRT in 11 days, across all versions |
| Cache hit rate | 99.96% | 2.7 million PHP 8.4 requests over the last 4 days |
The server hosts 74 PHP-FPM pools, spread across several PHP versions. Each site has its own Linux user and pool. Each PHP version has a single FPM master, and therefore a single OPcache shared by all its pools. Version 8.4 carries 52 of them: that is the one that got dedup.
The PHP packages are the usual distribution ones, rebuilt with the patch. Nothing changes as long as the directive is absent. Enabling it takes one file, loaded after the package's OPcache configuration:
; /etc/php/8.4/fpm/conf.d/99-opcache-dedup.ini
opcache.enable_dedup = 1
opcache.jit = disable
The rollout happened in two stages: one week on the development server (dedup enabled on several versions since October 22, no incidents), then production, on October 29 at 9:34 am, on version 8.4 only.
Two independent measurements give the same number.
From the outside. A probe reads the resident part of the 8.4 master's OPcache segment every five minutes, without going through PHP. Before October 29, it hovered between 4.3 and 4.5 GB. With dedup, same configuration, same sites: 2.5 GB. That is -43%.
From the inside. opcache_get_status() counts the memory that dedup avoided copying. Added to the memory used, it gives what the same content would take up without the patch: today 3.1 GB used and 2.3 GB saved, i.e. 5.4 GB without dedup. The gain is 43%, and above all, without the patch, this cache would no longer fit in its 5 GB: it would fill up every time it warmed up.
The bench predicted -46.5% on 80 copies of the same stack. Production mixes more WordPress and plugin versions: 82.5% of compiled functions find an identical block already stored, and the gain stays in the same range.
In eleven days, no worker died from SIGSEGV, SIGBUS or SIGABRT, on any version. The logs from the previous weeks, without dedup, contain three, between September 28 and October 22.
The server did, however, go through two out-of-memory episodes, on October 30 and November 4, where the kernel killed MariaDB and PHP workers. We analyzed them in detail, because suspicion is legitimate right after a production rollout:
Dedup reduces shared memory. It does not touch each worker's per-request memory. This is a pool sizing problem, which the graphs described below now let us fix pool by pool.
To follow the production rollout, we needed to see OPcache and the pools continuously, version by version, without exposing anything on the web.
opcache_get_status() in Prometheus format. It cannot be reached over HTTP: a probe calls it every minute directly over FastCGI, on the socket of one pool per version, just as the web server would. It rejects any request that comes through a web server.pm.status_path, also over direct FastCGI) and its workers' memory from /proc.The "OPcache full" alert fired within an hour of going live.
The OPcache dashboard showed version 8.4 as "full" with 2.7 GB free out of 5. The culprits: two limits that dedup had pushed ahead of memory:
opcache.max_accelerated_files, then set to 200,000,Once either limit is reached, OPcache stops caching new scripts: they are recompiled on every request. We raised max_accelerated_files to 1,000,000 and interned_strings_buffer to 256 MB on November 5. Four days later, the key table is at 32%, but interned strings are already at 91%: they will be the next limit.
The lesson applies to any dedup rollout. By freeing memory, it lets more scripts be cached, and OPcache's other limits come sooner. They need to be sized together: keys, interned strings and memory.
The memory graph shows drops at irregular intervals: seven full flushes of the 8.4 cache in four days, two of them around 3 am. They are neither restarts nor saturation: the manual_restarts counter attributes them to calls to opcache_reset().
This function appears in the code of several common WordPress plugins, for caching or remote management, which may call it after an update or after flushing their own cache. On a server where one master serves 52 sites, an action on a single site flushes the cache of the other 51, which recompile everything in the following minutes.
This behavior has nothing to do with dedup: it is stock OPcache. So is the fix: opcache.restrict_api, which restricts the OPcache API to an administration directory. We were already recommending it to shut off enumeration of other sites' scripts (part 1). Production shows it also protects availability.
The table referencing shared blocks holds 404,109 entries out of 786,432 usable (51%), i.e. 1.8 unique blocks per cached script. Its size depends on max_accelerated_files and caps at 2²⁰ slots of 32 bytes, i.e. 32 MB. On the development server, which is smaller, it had reached 93%. A full table breaks nothing: new blocks simply stop being shared. But this finding argues for a configurable size, a question we will raise with the maintainers.
The PHP-FPM dashboard gives, for each pool, the peak of active workers, how many times the limit was hit, and the real memory (PSS) of its workers. Of the server's 74 pools, all allowed to scale up to 32 workers, 15 never used more than two over two days of data, and only five hit the limit. The uniform setting let every small site reserve enough to bring the server down. This is the work in progress to put an end to the out-of-memory episodes.
max_accelerated_files and interned_strings_buffer become the bottleneck before memory does.opcache.restrict_api is essential as soon as several sites share a master, dedup or not.opcache_get_status(), no dedicated tool needed.Dedup stays enabled on 8.4 and will be extended to 8.2, 8.3 and 8.5 once the pools are resized. On the PHP side, the patch is ready for the development branch. We are proposing it to the maintainers, with these production numbers on top of the bench ones.
Measurements from October 21 to November 9, 2026 on our production server (PHP 8.4.27, 52 pools).
Website design, e-commerce or SEO: our web agency in Paris, Besançon and Normandy can help.
Free quoteHosting our clients' websites means taking on technical challenges every day. Today I'd like to share one of them, which…
Read