PHP OPcache configuration for WordPress: settings, tuning, and monitoring

OPcache is the single largest free performance win on a self-hosted WordPress server, but the default 128 MB allocation is too small for a plugin-heavy site and the default revalidation behavior is optimized for development, not production. This guide covers the six directives that matter for WordPress, a safe production profile, whether to enable JIT, how to read opcache_get_status output, and the cache-reset discipline you need when opcache.validate_timestamps is 0.

OPcache sits below every other cache on a WordPress server. Page caching and object caching skip work at the WordPress application layer. OPcache skips work one level below that: the PHP parser and compiler themselves. Every uncached request, every logged-in admin request, every REST API call, every WP-CLI command still has to execute PHP. OPcache decides whether that PHP is compiled from source on every request or served from pre-compiled bytecode in shared memory.

This article is the practical how-to: which directives actually matter for WordPress, what values to set them to, when to enable JIT, how to verify the cache is working, and what changes you have to make to your deployment workflow if you turn off timestamp validation.

Goal of this article

By the end of this guide you will have a production-grade OPcache configuration tuned for a typical WordPress or WooCommerce install, know how to read the output of opcache_get_status() to confirm the cache is healthy, and know which cache-reset step has to be part of your deploy script.

Prerequisites

  • PHP 8.2 or newer (this guide assumes php-fpm as the SAPI; the directives behave the same on PHP 8.2, 8.3, 8.4, and 8.5, with a handful of version-specific differences called out below)
  • Shell access to the server and permission to edit php.ini and reload PHP-FPM
  • A WordPress install to tune against; concrete values in the worked example assume a site with 25 to 40 active plugins, which is typical for a WooCommerce store
  • Familiarity with where your distribution stores the OPcache ini fragment. On Debian and Ubuntu it is /etc/php/8.x/mods-available/opcache.ini, symlinked from the conf.d directories. On Red Hat derivatives it is /etc/php.d/10-opcache.ini.

If you run WordPress on a managed host (Pantheon, WP Engine, Kinsta, Pressable, SiteGround), OPcache is managed for you, cache resets happen through the host's deploy workflow, and there is usually no way to edit the ini. This guide is for self-hosted environments where you own the PHP configuration.

What OPcache does (and what it does not)

OPcache is the bytecode cache built into PHP. When PHP executes a script for the first time, it tokenizes the source, parses it, and compiles it to Zend opcodes. OPcache stores those opcodes in shared memory so the next request can skip straight to execution. The PHP manual frames it as "caching the precompiled script bytecode in shared memory, thereby removing the need for PHP to load and parse scripts on each request."

A plugin-heavy WordPress request loads hundreds of PHP files. Without OPcache, every one of those files is re-parsed and re-compiled on every request. With OPcache configured correctly, the parse-and-compile cost is paid once per PHP-FPM worker lifetime and then amortized across thousands of requests. The benchmarks on typical WordPress installs are consistent: Pantheon's overview describes OPcache as "eliminating repeated compilation across tens of thousands of PHP files" with the result being "far lower server overhead and much faster time to first byte (TTFB)". Reports on mid-sized WordPress and WooCommerce sites routinely show 300 to 500 ms shaved off TTFB on the uncached request path after a correct OPcache configuration, plus a measurable drop in CPU usage during traffic spikes.

OPcache does not cache HTML, database query results, or any other application data. It caches one thing only: compiled PHP bytecode. It is orthogonal to the other cache layers: it helps every single uncached PHP request (including the ones that bypass page caching), but it does nothing about a slow database query, a synchronous external API call, or a hot spot in your theme.

One more version note that changes how to think about OPcache on new installs: starting in PHP 8.5, OPcache is compiled into PHP by default and is no longer an optional extension. On PHP 8.5+ you cannot accidentally ship a PHP build without OPcache available. On PHP 8.4 and earlier it is still technically optional, which means on a Dockerfile or a from-source build it is possible to end up without the extension loaded, so always confirm it is present before tuning it.

Step 1: verify OPcache is loaded

Before changing anything, confirm OPcache is actually enabled and measure what the current configuration looks like.

# Confirm the extension is loaded
php -i | grep -i "opcache"

You should see a block that includes Opcode Caching: Up and Running and the current values for every OPcache directive. If that line says Opcode Caching: Disabled or you do not see an OPcache block at all, the extension is not loaded and no tuning will help until you install php-opcache (Debian/Ubuntu) or php-opcache (Red Hat) and reload PHP-FPM.

Expected output (abbreviated):

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

To see the live numbers as PHP-FPM sees them (which is what actually matters for your web traffic, because CLI has a separate cache), place a small status script in your docroot:

<?php
// /wp-content/plugins/opcache-status.php
// Remove after use. Do not leave in production.
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 it at https://yoursite.nl/wp-content/plugins/opcache-status.php?token=CHANGE_ME and you get the full status object. The PHP manual documents every field; the ones that matter for tuning are 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, and opcache_statistics.oom_restarts.

Delete the status script after you are done. opcache_get_status() is considered information disclosure if exposed publicly.

Step 2: the six directives that matter for WordPress

OPcache has dozens of ini directives. On a WordPress site, six of them decide whether OPcache is working or wasting memory.

opcache.memory_consumption

What it does: total shared memory allocated to OPcache, in megabytes. Default 128.

What to set it to for WordPress: 256 MB is a safe starting value for a typical WordPress install with 20 to 30 plugins. For WooCommerce, multisite, or installs with 40+ plugins, start at 384 MB or 512 MB. Under-allocation is the most common OPcache misconfiguration I see on self-hosted WordPress: the default 128 MB fills up on a plugin-heavy site, OPcache evicts entries to make room, hit rate collapses, and every eviction means the next request recompiles from source.

The warning sign is opcache_statistics.oom_restarts (out-of-memory restarts) increasing over time in opcache_get_status(). If that counter is non-zero and keeps growing, your memory_consumption is too small.

opcache.interned_strings_buffer

What it does: memory for interned strings (deduplicated copies of identical strings across cached scripts), in megabytes. Default 8.

What to set it to for WordPress: 16 MB is a reasonable minimum; 32 MB is better for WooCommerce and plugin-heavy installs. WordPress code contains a lot of repeated string literals (hook names, option keys, translation strings) and the interned strings buffer is what keeps those from consuming cache memory multiple times. The 8 MB default runs out quickly on a large install and the symptom is the same as under-allocated memory_consumption: evictions and recompilation.

The warning sign is interned_strings_usage.free_memory approaching zero in the status output, and interned_strings_usage.number_of_strings climbing toward the cap.

Note: the interned strings buffer is carved out of opcache.memory_consumption. If you set memory_consumption = 256 and interned_strings_buffer = 32, the actual bytecode cache gets 224 MB.

opcache.max_accelerated_files

What it does: the maximum number of scripts OPcache will cache. Default 10000. The value is rounded up to the next prime number in the set {223, 463, 983, 1979, 3907, 7963, 16229, 32531, 65407, 130987, 262237, 524521, 1048793}.

What to set it to for WordPress: count the PHP files in your install and double it. A minimal WordPress install is around 3,000 PHP files. A site with 20 plugins and a typical theme is 8,000 to 15,000 files. WooCommerce alone adds several thousand. For most production WordPress sites, 32531 (the next prime above 20,000) is a good target. For WooCommerce or multisite with many plugins, use 65407.

To count your actual PHP files:

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

Set max_accelerated_files to comfortably exceed that number. If the value is too low, OPcache stops caching new files once the cap is hit. The warning sign is opcache_statistics.num_cached_keys approaching opcache_statistics.max_cached_keys in the status output.

opcache.validate_timestamps

What it does: controls whether OPcache checks the filesystem to see if a cached script is stale. Default 1 (check timestamps on every request, or every revalidate_freq seconds).

What to set it to for WordPress: for most self-hosted WordPress sites, leave this at 1. Timestamp validation is what lets a plugin update, a theme change, or a WP-CLI edit actually take effect on the next request. Disabling it (setting to 0) is the highest-performance option because PHP-FPM skips a stat() syscall on every included file, but it also means cached bytecode outlives the source file on disk, and anything short of a PHP-FPM reload or an explicit opcache_reset() will continue to serve the old code.

Set validate_timestamps = 0 only if you have a controlled deployment pipeline that runs opcache_reset() (or reloads PHP-FPM) as part of every deploy. Miss that step and your plugin update, or worse, a security patch, will appear to deploy successfully and then not actually take effect until the worker process is recycled for an unrelated reason.

opcache.revalidate_freq

What it does: how often OPcache checks script timestamps, in seconds, when validate_timestamps = 1. Default 2. Ignored when validate_timestamps = 0.

What to set it to for WordPress: 60 is a good production value. The default of 2 seconds means a timestamp check almost every request, which is wasteful on a busy site. 60 seconds means a cached script can be at most one minute stale, which is fine for any WordPress workflow that is not a live-edit development loop. Setting it to 0 forces a check on every request, which is appropriate for local development and nowhere else.

opcache.save_comments

What it does: whether to keep PHP docblock comments in cached bytecode. Default 1.

What to set it to for WordPress: leave this at 1. Setting it to 0 shrinks cached bytecode but breaks any code that reads docblocks at runtime. The PHP manual warns explicitly: "Disabling this configuration directive may break applications and frameworks that rely on comment parsing for annotations, including Doctrine, Zend Framework 2 and PHPUnit." Among WordPress plugins, several use docblock parsing for hooks, REST API routes, and CLI command discovery. The memory savings from save_comments = 0 are typically a few percent; the risk of a hard-to-diagnose plugin failure is not worth it.

Step 3: a production configuration profile

Drop the following into your OPcache ini fragment (for PHP 8.3 on Debian, /etc/php/8.3/mods-available/opcache.ini). Every line is an explicit override of the default; lines that match the default are included anyway so the resulting file is self-documenting.

; OPcache production configuration for WordPress
; Tuned for a site with 25 to 40 active plugins, WooCommerce, and PHP-FPM

; --- Enable the cache ---
opcache.enable=1
opcache.enable_cli=0              ; CLI has a separate cache; leave off unless needed

; --- Memory sizing ---
opcache.memory_consumption=256    ; total SHM budget in MB
opcache.interned_strings_buffer=32 ; carved out of memory_consumption
opcache.max_accelerated_files=32531 ; rounded up to prime internally

; --- Freshness policy ---
opcache.validate_timestamps=1     ; flip to 0 only with deploy-time opcache_reset
opcache.revalidate_freq=60        ; one check per file per minute
opcache.file_update_protection=2  ; don't cache files modified in the last 2s

; --- Safety ---
opcache.save_comments=1           ; required for annotation-based code
opcache.max_wasted_percentage=10  ; allow 10% fragmentation before restart

; --- JIT (see JIT section below) ---
opcache.jit=disable
opcache.jit_buffer_size=0

Expected output: after reloading PHP-FPM with sudo systemctl reload php8.3-fpm and serving some traffic, opcache_get_status() should report:

  • opcache_enabled: true
  • cache_full: false
  • memory_usage.used_memory: growing to a stable value well below memory_consumption
  • opcache_statistics.opcache_hit_rate: 98% or higher after the cache warms
  • opcache_statistics.oom_restarts: 0
  • opcache_statistics.num_cached_scripts: a stable number, well below max_accelerated_files

If the hit rate sits below 95% after an hour of traffic, something is forcing recompilation. The usual culprit is that validate_timestamps=1 combined with a deploy tool that rewrites file timestamps on every rsync. Check that your deploy preserves mtime.

For the WooCommerce/multisite profile, override these two lines:

opcache.memory_consumption=512
opcache.max_accelerated_files=65407

Step 4: decide whether to enable JIT

PHP 8.0 introduced a JIT compiler that sits on top of OPcache. For compute-heavy workloads (image processing, scientific calculation, raw PHP benchmarks), JIT can deliver real speedups. For WordPress, it almost never does.

WordPress is I/O-bound: the vast majority of time in a WordPress request is spent waiting on database queries, Redis lookups, external HTTP calls, and filesystem reads. JIT only speeds up PHP code execution time, which is a small fraction of the total. Benchmarks on WordPress typically show 1 to 5 percent improvement from enabling JIT, in exchange for additional memory (the JIT buffer) and occasional stability regressions in edge cases.

My rule: leave JIT disabled on WordPress production unless you have a specific measured workload that benefits. The setting is:

opcache.jit=disable
opcache.jit_buffer_size=0

Version note: in PHP 8.3 and earlier, opcache.jit defaulted to tracing and opcache.jit_buffer_size defaulted to 0. In PHP 8.4, those defaults changed: opcache.jit now defaults to disable and opcache.jit_buffer_size now defaults to 64M. Either way, if you want JIT off explicitly, set both directives as above.

If you do want to experiment with JIT, the PHP manual documents the mode codes. A minimal WordPress-friendly JIT setting would be:

opcache.jit=tracing
opcache.jit_buffer_size=128M

Then measure. Compare TTFB and CPU usage before and after on the same real-world traffic pattern. If you cannot measure a difference, turn it back off.

Step 5: verify the cache is healthy

Reload PHP-FPM, let the site run under traffic for at least 30 minutes, then re-check opcache_get_status(). A healthy OPcache on WordPress looks like this:

Metric Healthy value What it means if it is wrong
opcache_enabled true Extension not loaded or opcache.enable=0
cache_full false memory_consumption too small
opcache_statistics.opcache_hit_rate 98% or higher Something is forcing recompilation
opcache_statistics.oom_restarts 0 memory_consumption too small; cache had to evict
opcache_statistics.hash_restarts 0 max_accelerated_files too small
opcache_statistics.num_cached_keys Well below max_cached_keys Room to cache more scripts
memory_usage.free_memory Stable, non-zero Still has headroom for new scripts
interned_strings_usage.free_memory Non-zero interned_strings_buffer not exhausted

A hit rate above 98% is the target for a well-tuned WordPress site. Anything below 95% means OPcache is doing less work than it should.

The WordPress core team also added a Site Health check for OPcache in core trac ticket #63697; on recent WordPress versions you can see whether OPcache is enabled under Tools → Site Health → Info → Server. That check is a pass/fail on extension presence, not a tuning diagnostic, so it is a starting point rather than a replacement for reading opcache_get_status() directly.

Step 6: cache-reset discipline for deploys

OPcache caches bytecode keyed by script path and (by default) timestamp. When you deploy a code change, one of three things has to happen before the new code takes effect:

  1. If validate_timestamps=1: OPcache notices the new timestamp within revalidate_freq seconds (60 in the profile above) and automatically recompiles. You do not need to do anything. This is the default and is correct for most WordPress sites.
  2. If validate_timestamps=0 and you just deployed: OPcache has no idea the file on disk changed. You have to explicitly invalidate the cache for the new code to run. Without this step, your deploy appears to succeed and then does nothing visible until the worker is recycled for an unrelated reason (for example, pm.max_requests rotation, which my PHP-FPM tuning guide covers).
  3. If you reload PHP-FPM: sudo systemctl reload php8.3-fpm wipes every worker's cache along with restarting the workers. This is heavier than opcache_reset() but guarantees a clean slate.

For option 2, you have two mechanisms:

Option 2a: reload PHP-FPM. This is the simplest and always works:

sudo systemctl reload php8.3-fpm

Option 2b: call opcache_reset() via HTTP. Useful when your deploy user cannot sudo. Place a one-line script in the docroot, protected by a secret:

<?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';

Then your deploy script curls it after the file sync:

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

Important caveat: opcache_reset() is per-SAPI. Calling it from the CLI (php -r "opcache_reset();") resets the CLI cache, not the PHP-FPM cache. It has to be called from a request that runs under PHP-FPM.

Common troubleshooting

Cache is "up and running" but hit rate stays at 70-80%. Traffic is being served by a newly-started or recently-reset worker. Let it warm for a full 30 minutes under real traffic before judging the hit rate. If it still will not climb, check oom_restarts and hash_restarts.

Every plugin update breaks the site for 60 seconds. You have validate_timestamps=0 or revalidate_freq set too high. For a production site where you update plugins from wp-admin rather than through a deploy pipeline, keep validate_timestamps=1 and revalidate_freq at 60 or lower.

cache_full: true within an hour of restarting PHP-FPM. memory_consumption is too small for your code base. Double it and reload PHP-FPM.

Hit rate is 99% but TTFB is still high. OPcache is doing its job. The bottleneck is elsewhere. What TTFB actually measures lists the other causes: slow database queries, synchronous external API calls, exhausted PHP workers. OPcache is a floor, not a ceiling.

Dashboard shows opcache_enabled: false but php -i shows it as enabled. You are looking at the CLI cache. php -i runs under the CLI SAPI, which has opcache.enable_cli controlling it (off by default). To see the PHP-FPM cache, hit the status script over HTTP.

Complete final configuration

For copy-paste on a typical production WordPress server (PHP 8.3 on Debian, Ubuntu, or a derivative), the full OPcache ini fragment is:

; /etc/php/8.3/mods-available/opcache.ini
; OPcache tuned for WordPress production (25 to 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

Reload PHP-FPM after saving:

sudo systemctl reload php8.3-fpm

Then verify:

# Confirm active settings
php-fpm8.3 -i | grep -E "opcache\.(memory_consumption|max_accelerated_files|validate_timestamps|jit)"

Serve traffic for 30 minutes, then check opcache_get_status() via a protected web-accessible script. Target a hit rate above 98%, oom_restarts at 0, and num_cached_keys well below max_cached_keys. If all three are healthy, the cache is sized correctly.

Done chasing slowdowns?

Performance issues tend to come back after quick fixes. WordPress maintenance keeps updates, caching and limits consistent.

See WordPress maintenance

Search this site

Start typing to search, or browse the knowledge base and blog.