PHP OPcache configureren voor WordPress: instellingen, tuning en monitoring

OPcache is de grootste gratis performance-winst op een self-hosted WordPress-server, maar de standaard 128 MB is te krap voor een site met veel plugins en het standaard revalidatie-gedrag is afgesteld op development, niet op productie. Deze gids behandelt de zes directieven die ertoe doen voor WordPress, een veilig productieprofiel, of je JIT moet aanzetten, hoe je de output van opcache_get_status leest, en welke cache-reset discipline je nodig hebt zodra opcache.validate_timestamps op 0 staat.

OPcache zit onder elke andere cache op een WordPress-server. Page cache en object cache slaan werk over op het WordPress-applicatieniveau. OPcache slaat werk over op het niveau daaronder: de PHP-parser en -compiler zelf. Elk niet-gecacht verzoek, elk ingelogd admin-verzoek, elke REST API-aanroep, elk WP-CLI commando moet nog steeds PHP uitvoeren. OPcache bepaalt of die PHP bij elk verzoek opnieuw wordt gecompileerd, of uit voorgecompileerde bytecode in shared memory komt.

Dit artikel is de praktische how-to: welke directieven er echt toe doen voor WordPress, wat je ze moet geven, wanneer je JIT aanzet, hoe je verifieert dat de cache werkt, en welke aanpassingen je moet doen in je deploy-workflow als je timestamp-validatie uitzet.

Doel van dit artikel

Aan het einde van deze gids heb je een productie-waardige OPcache-configuratie die past bij een typische WordPress- of WooCommerce-installatie, weet je hoe je opcache_get_status() leest om te bevestigen dat de cache gezond is, en weet je welke cache-reset-stap in je deploy-script hoort te staan.

Voorwaarden

  • PHP 8.2 of nieuwer (deze gids gaat uit van php-fpm als SAPI; de directieven gedragen zich gelijk op PHP 8.2, 8.3, 8.4 en 8.5, met een paar versie-specifieke verschillen die ik expliciet benoem)
  • Shell-toegang tot de server en rechten om php.ini aan te passen en PHP-FPM te herladen
  • Een WordPress-installatie om tegen te tunen; de concrete waarden in het voorbeeld gaan uit van 25 tot 40 actieve plugins, wat typisch is voor een WooCommerce-shop
  • Bekend zijn met waar je distributie het OPcache ini-fragment bewaart. Op Debian en Ubuntu is dat /etc/php/8.x/mods-available/opcache.ini, met symlinks vanuit de conf.d-mappen. Op Red Hat-derivaten staat het in /etc/php.d/10-opcache.ini.

Draai je WordPress bij een managed host (Pantheon, WP Engine, Kinsta, Pressable, SiteGround)? Dan regelt de host OPcache voor je, worden cache-resets via hun deploy-workflow gedaan, en is er meestal geen manier om de ini zelf aan te passen. Deze gids is voor self-hosted omgevingen waar je de PHP-configuratie in eigen hand hebt.

Wat OPcache wel en niet doet

OPcache is de bytecode-cache die in PHP zelf is ingebouwd. Als PHP een script voor het eerst uitvoert, tokeniseert het de source, parset het en compileert het naar Zend opcodes. OPcache bewaart die opcodes in shared memory zodat het volgende verzoek direct naar uitvoeren kan. De PHP-handleiding omschrijft het als "het cachen van de voorgecompileerde script-bytecode in shared memory, waardoor PHP scripts niet bij elk verzoek hoeft te laden en te parsen".

Een WordPress-verzoek met veel plugins laadt honderden PHP-bestanden. Zonder OPcache wordt elk van die bestanden bij elk verzoek opnieuw geparst en gecompileerd. Met OPcache goed ingesteld betaal je die parse-en-compile-kost één keer per PHP-FPM worker-leven, en deel je hem vervolgens uit over duizenden verzoeken. De benchmarks op typische WordPress-installaties zijn consistent: Pantheon beschrijft OPcache als "het elimineren van herhaalde compilatie over tienduizenden PHP-bestanden", met als gevolg "aanzienlijk minder server-overhead en veel snellere time to first byte". Rapporten van middelgrote WordPress- en WooCommerce-sites laten standaard 300 tot 500 ms minder TTFB zien op het niet-gecachte pad na een juiste OPcache-configuratie, plus een meetbare daling in CPU-gebruik tijdens verkeerspieken.

OPcache cacht geen HTML, geen database-queryresultaten, geen andere applicatiedata. Het cacht één ding: gecompileerde PHP-bytecode. Het staat los van de andere cache-lagen: het helpt elk niet-gecacht PHP-verzoek (inclusief de verzoeken die de page cache overslaan), maar het doet niets aan een trage database-query, een synchrone externe API-call, of een hot spot in je thema.

Nog één versie-opmerking die verandert hoe je OPcache op nieuwe installaties benadert: vanaf PHP 8.5 is OPcache in PHP zelf gecompileerd en geen optionele extensie meer. Op PHP 8.5+ kun je niet per ongeluk een PHP-build zonder OPcache uitrollen. Op PHP 8.4 en ouder is het technisch nog optioneel, dus op een Dockerfile of een from-source build kun je zomaar zonder de extensie eindigen. Controleer dus altijd eerst of hij geladen is voor je gaat tunen.

Stap 1: controleer of OPcache geladen is

Voor je iets wijzigt, bevestig dat OPcache daadwerkelijk aanstaat en meet hoe de huidige configuratie eruitziet.

# Controleer of de extensie geladen is
php -i | grep -i "opcache"

Je zou een blok moeten zien met onder andere Opcode Caching: Up and Running en de huidige waarden van elke OPcache-directive. Staat er Opcode Caching: Disabled of zie je helemaal geen OPcache-blok? Dan is de extensie niet geladen en helpt geen enkele tuning tot je php-opcache (Debian/Ubuntu) of php-opcache (Red Hat) installeert en PHP-FPM herlaadt.

Verwachte output (ingekort):

opcache
Opcode Caching => Up and Running
Optimization => On
SHM Cache => Enabled
File Cache => Disabled
JIT => Disabled
Startup => OK
Shared memory model => mmap
...

Om de live cijfers te zien zoals PHP-FPM ze ziet (en dát is wat telt voor je webverkeer, want CLI heeft een eigen cache), plaats je een klein statusscript in je docroot:

<?php
// /wp-content/plugins/opcache-status.php
// Verwijder na gebruik. Niet laten staan op productie.
if (!isset($_GET['token']) || $_GET['token'] !== 'CHANGE_ME') {
    http_response_code(403);
    exit;
}
header('Content-Type: application/json');
echo json_encode(opcache_get_status(false), JSON_PRETTY_PRINT);

Hit hem op https://jouwsite.nl/wp-content/plugins/opcache-status.php?token=CHANGE_ME en je krijgt het volledige statusobject terug. De PHP-handleiding documenteert elk veld; de velden die tellen voor tuning zijn memory_usage, interned_strings_usage, opcache_statistics.num_cached_scripts, opcache_statistics.num_cached_keys, opcache_statistics.max_cached_keys, opcache_statistics.opcache_hit_rate en opcache_statistics.oom_restarts.

Verwijder het statusscript zodra je klaar bent. opcache_get_status() geldt als informatielek als je hem publiek laat staan.

Stap 2: de zes directieven die ertoe doen voor WordPress

OPcache heeft tientallen ini-directieven. Op een WordPress-site bepalen zes ervan of OPcache werkt, of gewoon geheugen verspilt.

opcache.memory_consumption

Wat het doet: totaal shared memory voor OPcache, in megabytes. Standaard 128.

Wat je instelt voor WordPress: 256 MB is een veilig startpunt voor een typische WordPress-installatie met 20 tot 30 plugins. Voor WooCommerce, multisite of installaties met 40+ plugins begin je op 384 MB of 512 MB. Te weinig geheugen toewijzen is de meest voorkomende OPcache-misconfiguratie die ik zie op self-hosted WordPress: de standaard 128 MB raakt vol op een plugin-rijke site, OPcache gooit entries eruit om ruimte te maken, de hit rate zakt in, en bij elke eviction moet het volgende verzoek opnieuw compileren vanaf source.

Het signaal is opcache_statistics.oom_restarts (out-of-memory restarts) die in de loop van de tijd oplopen in opcache_get_status(). Staat die teller niet op nul en blijft hij groeien? Dan is je memory_consumption te klein.

opcache.interned_strings_buffer

Wat het doet: geheugen voor interned strings (gedeelde kopieën van identieke strings over gecachte scripts heen), in megabytes. Standaard 8.

Wat je instelt voor WordPress: 16 MB is een redelijk minimum; 32 MB is beter voor WooCommerce en plugin-rijke installaties. WordPress-code bevat veel herhaalde string-literals (hook-namen, option-keys, vertaalstrings) en de interned strings buffer zorgt dat die niet meermaals cache-geheugen vreten. De standaard 8 MB raakt snel vol op een grote installatie, en het symptoom is hetzelfde als bij te weinig memory_consumption: evictions en hercompilatie.

Het signaal is interned_strings_usage.free_memory dat tegen nul loopt in de status-output, en interned_strings_usage.number_of_strings dat tegen het plafond aantikt.

Let op: de interned strings buffer wordt uit opcache.memory_consumption gehaald. Zet je memory_consumption = 256 en interned_strings_buffer = 32, dan krijgt de eigenlijke bytecode-cache dus 224 MB.

opcache.max_accelerated_files

Wat het doet: het maximum aantal scripts dat OPcache cacht. Standaard 10000. De waarde wordt intern afgerond naar het eerstvolgende priemgetal uit de set {223, 463, 983, 1979, 3907, 7963, 16229, 32531, 65407, 130987, 262237, 524521, 1048793}.

Wat je instelt voor WordPress: tel de PHP-bestanden in je installatie en neem het dubbele. Een minimale WordPress-installatie is zo'n 3.000 PHP-bestanden. Een site met 20 plugins en een typisch thema zit op 8.000 tot 15.000 bestanden. WooCommerce alleen al voegt er duizenden toe. Voor de meeste productie-WordPress-sites is 32531 (het priemgetal net boven 20.000) een goede keuze. Voor WooCommerce of multisite met veel plugins gebruik je 65407.

Om je daadwerkelijke PHP-bestanden te tellen:

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

Zet max_accelerated_files comfortabel hoger dan dat getal. Is de waarde te laag, dan stopt OPcache met het cachen van nieuwe bestanden zodra het plafond is bereikt. Het signaal is opcache_statistics.num_cached_keys die tegen opcache_statistics.max_cached_keys aankruipt in de status-output.

opcache.validate_timestamps

Wat het doet: bepaalt of OPcache het filesystem controleert om te zien of een gecacht script verouderd is. Standaard 1 (bij elk verzoek, of elke revalidate_freq seconden).

Wat je instelt voor WordPress: voor de meeste self-hosted WordPress-sites laat je dit op 1. Timestamp-validatie is wat ervoor zorgt dat een plugin-update, een themawijziging of een WP-CLI-bewerking bij het volgende verzoek daadwerkelijk effect heeft. Uitzetten (op 0) is de hoogste-performance optie want PHP-FPM slaat dan een stat()-syscall over op elk geïncludeerd bestand, maar het betekent ook dat gecachte bytecode blijft leven nadat het bronbestand op schijf is vervangen. Zonder een expliciete PHP-FPM reload of een opcache_reset() blijft de oude code gewoon draaien.

Zet validate_timestamps = 0 alleen als je een gecontroleerde deploy-pipeline hebt die bij elke deploy opcache_reset() draait (of PHP-FPM herlaadt). Vergeet die stap en je plugin-update, of erger nog, een security-patch, lijkt succesvol gedeployt en heeft daarna gewoon geen effect tot het worker-proces om een andere reden wordt gerecycled.

opcache.revalidate_freq

Wat het doet: hoe vaak OPcache timestamps controleert, in seconden, als validate_timestamps = 1. Standaard 2. Wordt genegeerd bij validate_timestamps = 0.

Wat je instelt voor WordPress: 60 is een goede productiewaarde. De standaard van 2 seconden betekent een timestamp-check bij zo goed als elk verzoek, wat verspilling is op een drukke site. 60 seconden betekent dat een gecacht script maximaal één minuut verouderd kan zijn, wat prima is voor elke WordPress-workflow die geen live-edit development-loop is. 0 forceert een check bij elk verzoek, wat alleen zinnig is voor lokale development.

opcache.save_comments

Wat het doet: of PHP-docblock-comments worden bewaard in de gecachte bytecode. Standaard 1.

Wat je instelt voor WordPress: laat dit op 1. Op 0 zetten maakt de gecachte bytecode kleiner, maar breekt elke code die docblocks runtime leest. De PHP-handleiding waarschuwt expliciet: "Het uitzetten van deze directive kan applicaties en frameworks breken die commentaar-parsing gebruiken voor annotaties, waaronder Doctrine, Zend Framework 2 en PHPUnit." Onder WordPress-plugins gebruiken er meerdere docblock-parsing voor hooks, REST API-routes en CLI-commando-discovery. De geheugenbesparing van save_comments = 0 is doorgaans een paar procent; het risico op een lastig te diagnosticeren plugin-crash weegt daar niet tegen op.

Stap 3: een productieconfiguratie-profiel

Zet onderstaande in je OPcache ini-fragment (voor PHP 8.3 op Debian is dat /etc/php/8.3/mods-available/opcache.ini). Elke regel is een expliciete override van de standaard; regels die de standaardwaarde hebben zijn toch opgenomen zodat het bestand zelfdocumenterend is.

; OPcache productieconfiguratie voor WordPress
; Afgestemd op een site met 25 tot 40 actieve plugins, WooCommerce en PHP-FPM

; --- Cache aanzetten ---
opcache.enable=1
opcache.enable_cli=0              ; CLI heeft een eigen cache; laat hem uit tenzij nodig

; --- Geheugen-sizing ---
opcache.memory_consumption=256    ; totaal SHM-budget in MB
opcache.interned_strings_buffer=32 ; wordt uit memory_consumption gehaald
opcache.max_accelerated_files=32531 ; intern afgerond naar priemgetal

; --- Versheid-beleid ---
opcache.validate_timestamps=1     ; alleen op 0 met opcache_reset in je deploy
opcache.revalidate_freq=60        ; één check per bestand per minuut
opcache.file_update_protection=2  ; cache geen bestanden die <2s geleden zijn geraakt

; --- Veiligheid ---
opcache.save_comments=1           ; nodig voor annotation-based code
opcache.max_wasted_percentage=10  ; 10% fragmentatie voor een restart

; --- JIT (zie de JIT-sectie hieronder) ---
opcache.jit=disable
opcache.jit_buffer_size=0

Verwachte output: na een reload van PHP-FPM met sudo systemctl reload php8.3-fpm en wat verkeer, hoort opcache_get_status() dit terug te geven:

  • opcache_enabled: true
  • cache_full: false
  • memory_usage.used_memory: groeit naar een stabiele waarde ver onder memory_consumption
  • opcache_statistics.opcache_hit_rate: 98% of hoger nadat de cache warm is
  • opcache_statistics.oom_restarts: 0
  • opcache_statistics.num_cached_scripts: een stabiel getal, ver onder max_accelerated_files

Blijft de hit rate na een uur verkeer onder de 95%? Dan forceert iets hercompilatie. Meestal is de boosdoener validate_timestamps=1 in combinatie met een deploy-tool die bij elke rsync de mtimes herschrijft. Check dat je deploy de mtime bewaart.

Voor het WooCommerce- of multisite-profiel override je deze twee regels:

opcache.memory_consumption=512
opcache.max_accelerated_files=65407

Stap 4: JIT aan of uit?

PHP 8.0 introduceerde een JIT-compiler die bovenop OPcache draait. Voor rekenintensieve workloads (beeldbewerking, wetenschappelijke berekeningen, ruwe PHP-benchmarks) kan JIT echt winst opleveren. Voor WordPress bijna nooit.

WordPress is I/O-bound: het grootste deel van de tijd in een WordPress-verzoek gaat zitten in database-queries, Redis-lookups, externe HTTP-calls en filesystem-reads. JIT versnelt alleen de pure PHP-uitvoertijd, en dat is een klein percentage van het totaal. Benchmarks op WordPress laten standaard 1 tot 5 procent verbetering zien door JIT aan te zetten, in ruil voor extra geheugen (de JIT-buffer) en af en toe een stabiliteitsprobleem in edge cases.

Mijn regel: laat JIT uit op WordPress-productie, tenzij je een specifiek gemeten workload hebt die er baat bij heeft. De instelling is:

opcache.jit=disable
opcache.jit_buffer_size=0

Versie-opmerking: in PHP 8.3 en ouder stond opcache.jit op tracing en opcache.jit_buffer_size op 0. In PHP 8.4 zijn die defaults veranderd: opcache.jit staat nu standaard op disable en opcache.jit_buffer_size op 64M. Wil je JIT expliciet uit, zet dan beide zoals hierboven.

Wil je toch experimenteren met JIT? De PHP-handleiding documenteert de mode-codes. Een minimale WordPress-vriendelijke JIT-instelling zou zijn:

opcache.jit=tracing
opcache.jit_buffer_size=128M

Meet het daarna. Vergelijk TTFB en CPU-gebruik voor en na op hetzelfde reële verkeerspatroon. Kun je geen verschil meten, zet hem dan gewoon weer uit.

Stap 5: controleer of de cache gezond is

Herlaad PHP-FPM, laat de site minstens 30 minuten onder verkeer draaien, en check opcache_get_status() opnieuw. Een gezonde OPcache op WordPress ziet er zo uit:

Metric Gezonde waarde Wat het betekent als het afwijkt
opcache_enabled true Extensie niet geladen of opcache.enable=0
cache_full false memory_consumption te klein
opcache_statistics.opcache_hit_rate 98% of hoger Iets forceert hercompilatie
opcache_statistics.oom_restarts 0 memory_consumption te klein; cache moest evicten
opcache_statistics.hash_restarts 0 max_accelerated_files te klein
opcache_statistics.num_cached_keys Ver onder max_cached_keys Ruimte genoeg voor meer scripts
memory_usage.free_memory Stabiel, niet-nul Nog headroom voor nieuwe scripts
interned_strings_usage.free_memory Niet-nul interned_strings_buffer niet uitgeput

Een hit rate boven de 98% is het doel voor een goed getunede WordPress-site. Alles onder 95% betekent dat OPcache minder werk doet dan het zou moeten.

Het WordPress core-team heeft in core trac ticket #63697 ook een Site Health-check voor OPcache toegevoegd; op recente WordPress-versies zie je onder Tools → Site Health → Info → Server of OPcache aanstaat. Die check is een pass/fail op extensie-aanwezigheid, geen tuning-diagnose, dus zie hem als een startpunt en niet als vervanging voor opcache_get_status() zelf lezen.

Stap 6: cache-reset discipline bij deploys

OPcache cacht bytecode op basis van scriptpath en (standaard) timestamp. Als je een code-wijziging deployt, moet een van deze drie dingen gebeuren voor de nieuwe code ook echt draait:

  1. Bij validate_timestamps=1: OPcache ziet binnen revalidate_freq seconden (60 in het profiel hierboven) de nieuwe timestamp en hercompileert automatisch. Je hoeft niets extra te doen. Dit is de default en is goed voor de meeste WordPress-sites.
  2. Bij validate_timestamps=0 en je hebt net gedeployt: OPcache weet niet dat het bestand op schijf is veranderd. Je moet de cache expliciet invalideren voordat de nieuwe code gaat draaien. Zonder die stap lijkt je deploy gewoon succesvol en gebeurt er vervolgens niets zichtbaars tot de worker om een andere reden wordt gerecycled (bijvoorbeeld via pm.max_requests-rotatie, zoals mijn PHP-FPM tuning-gids behandelt).
  3. Bij een PHP-FPM reload: sudo systemctl reload php8.3-fpm wist elke worker-cache en herstart ze. Dit is zwaarder dan opcache_reset() maar garandeert een schone lei.

Voor optie 2 heb je twee mechanismen:

Optie 2a: herlaad PHP-FPM. De simpelste en werkt altijd:

sudo systemctl reload php8.3-fpm

Optie 2b: roep opcache_reset() via HTTP aan. Handig als je deploy-gebruiker niet mag sudo'en. Plaats een one-liner in de docroot, beschermd met een geheime token:

<?php
// /wp-content/plugins/opcache-reset.php
if (!isset($_GET['token']) || !hash_equals('CHANGE_ME_LONG_RANDOM_STRING', $_GET['token'])) {
    http_response_code(403);
    exit;
}
echo opcache_reset() ? 'reset' : 'failed';

Je deploy-script curlt hem vervolgens na de bestandssync:

curl -s "https://jouwsite.nl/wp-content/plugins/opcache-reset.php?token=..."

Belangrijke kanttekening: opcache_reset() werkt per SAPI. Aanroepen vanuit de CLI (php -r "opcache_reset();") reset de CLI-cache, niet de PHP-FPM-cache. Hij moet worden aangeroepen vanuit een verzoek dat onder PHP-FPM draait.

Veelvoorkomende troubleshooting

De cache draait ("up and running") maar de hit rate blijft op 70-80% hangen. Het verkeer wordt bediend door een net gestarte of recent gereset worker. Laat hem 30 minuten onder echt verkeer warmdraaien voor je de hit rate beoordeelt. Blijft hij lager? Check oom_restarts en hash_restarts.

Elke plugin-update breekt de site 60 seconden lang. Je hebt validate_timestamps=0 of revalidate_freq staat te hoog. Op een productiesite waar plugins via wp-admin worden bijgewerkt in plaats van via een deploy-pipeline hou je validate_timestamps=1 en revalidate_freq op 60 of lager.

cache_full: true binnen een uur na herstart PHP-FPM. memory_consumption is te klein voor je code-base. Verdubbel hem en herlaad PHP-FPM.

Hit rate is 99% maar TTFB is nog steeds hoog. OPcache doet zijn werk. Het bottleneck zit ergens anders. Wat TTFB eigenlijk meet somt de andere oorzaken op: trage database-queries, synchrone externe API-calls, uitgeputte PHP workers. OPcache is een vloer, geen plafond.

Dashboard zegt opcache_enabled: false maar php -i zegt aan. Je kijkt naar de CLI-cache. php -i draait onder de CLI-SAPI, en die wordt gestuurd door opcache.enable_cli (standaard uit). Om de PHP-FPM-cache te zien, benader je het statusscript via HTTP.

Complete eindconfiguratie

Voor copy-paste op een typische productie-WordPress-server (PHP 8.3 op Debian, Ubuntu of afgeleide), is het volledige OPcache ini-fragment:

; /etc/php/8.3/mods-available/opcache.ini
; OPcache getuned voor WordPress productie (25 tot 40 plugins, WooCommerce)

zend_extension=opcache.so

opcache.enable=1
opcache.enable_cli=0

opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=32531

opcache.validate_timestamps=1
opcache.revalidate_freq=60
opcache.file_update_protection=2

opcache.save_comments=1
opcache.max_wasted_percentage=10

opcache.jit=disable
opcache.jit_buffer_size=0

Herlaad PHP-FPM na opslaan:

sudo systemctl reload php8.3-fpm

Verifieer dan:

# Actieve instellingen bevestigen
php-fpm8.3 -i | grep -E "opcache\.(memory_consumption|max_accelerated_files|validate_timestamps|jit)"

Laat 30 minuten verkeer lopen en check daarna opcache_get_status() via een beschermd webscript. Mik op een hit rate boven 98%, oom_restarts op 0, en num_cached_keys ruim onder max_cached_keys. Zijn die drie gezond, dan is de cache goed gesized.

Klaar met terugkerende traagheid?

Traagheid komt vaak terug na snelle fixes. Professioneel onderhoud houdt updates, caching en limieten consequent op orde.

Bekijk WordPress onderhoud

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.