OPcache tuning for WordPress means giving PHP enough shared memory and file slots to keep the compiled code of core, your theme and every plugin in memory, so no request has to recompile it. PHP-FPM tuning means running as many PHP workers as the server’s RAM and CPU can actually sustain, so requests don’t queue. Both show up as server response time on uncached requests. The right values depend on your codebase and traffic: count your PHP files, check opcache_get_status(), measure real worker memory and watch the FPM status page, instead of copying numbers from a blog post.
When a WordPress site has a slow server response and the page cache is fine, the PHP layer is one of the first places I look. The usual findings are an OPcache that’s full and restarting, a pool with five workers on a server with 16 GB of RAM, or the opposite: a pool so large it pushes the server into swap at peak. This page goes through the directives that matter and how to choose values for your own server. It belongs to my guide on how to speed up WordPress, and it’s the server-side follow-up to reducing TTFB in WordPress.
What do OPcache and PHP-FPM do for WordPress?
Every WordPress request loads hundreds of PHP files: core, the theme and all active plugins, whether or not the page uses them. Without OPcache, PHP reads, parses and compiles each of those files into bytecode on every single request. OPcache keeps the compiled bytecode in shared memory, and every worker reuses it.
PHP-FPM is the process manager that runs those workers. Each worker handles one request at a time. If ten requests arrive and the pool has eight workers, two wait in the listen queue, and that wait is added to their TTFB. It doesn’t show up in any WordPress profiler, because WordPress hasn’t started yet.
Neither one helps a page served from page cache, since PHP never runs. They matter for cache misses, logged-in users, wp-admin, WooCommerce checkout, the REST API, admin-ajax and cron. Those are also the requests a Redis object cache helps with, so the two pieces of work usually go together.
Which directives matter for OPcache tuning in WordPress?
These are the php.ini settings I review on every server. The defaults listed are PHP’s documented defaults. Your host or distribution may ship different ones, so check php -i | grep opcache or phpinfo() from a web request, since CLI and FPM can load different ini files.
| Directive | Default | What it controls | How to choose a value |
|---|---|---|---|
opcache.enable | 1 | OPcache for web requests | Must be on. Some hosts still ship it off |
opcache.memory_consumption | 128 (MB) | Shared memory for compiled scripts | Enough that free_memory stays comfortably above zero after a full day of traffic |
opcache.interned_strings_buffer | 8 (MB) | Memory for repeated strings such as class and function names | Raise it if the status shows the interned strings buffer full |
opcache.max_accelerated_files | 10000 | Maximum number of cached scripts | Above the number of PHP files on the server, with room for updates |
opcache.validate_timestamps | 1 | Whether PHP checks files for changes | Leave on if WordPress updates itself; see below |
opcache.revalidate_freq | 2 (seconds) | How often those checks happen | A higher value means fewer checks and slower pickup of changed files |
opcache.save_comments | 1 | Keeps docblocks in the cache | Leave on. Some libraries read annotations at runtime |
To size max_accelerated_files, count the PHP files of every site that shares the same PHP-FPM master, since they share one OPcache:
find /var/www -type f -name "*.php" | wc -lPHP rounds the value up to the next number in an internal list of primes, so the effective limit is a bit higher than what you set. A WooCommerce site with a page builder and forty plugins can easily pass the default of 10,000 files.
How do you check whether OPcache is big enough?
Read opcache_get_status() from a web request, not the CLI, because the CLI has its own separate cache. Put a small script behind authentication or an IP restriction, open it, then delete it:
<?php
$s = opcache_get_status( false );
printf( "Free memory: %.1f MB\n", $s['memory_usage']['free_memory'] / 1048576 );
printf( "Wasted memory: %.1f MB\n", $s['memory_usage']['wasted_memory'] / 1048576 );
printf( "Cached keys: %d of %d\n", $s['opcache_statistics']['num_cached_keys'], $s['opcache_statistics']['max_cached_keys'] );
printf( "Hit rate: %.2f%%\n", $s['opcache_statistics']['opcache_hit_rate'] );
printf( "OOM restarts: %d\n", $s['opcache_statistics']['oom_restarts'] );
printf( "Hash restarts: %d\n", $s['opcache_statistics']['hash_restarts'] );The counters that matter are the restarts. oom_restarts above zero means OPcache ran out of memory and threw everything away, so the next requests recompiled the whole codebase. hash_restarts above zero means it ran out of file slots. Either one is a reason to raise the matching directive. A high hit rate on its own doesn’t prove much, because it’s computed since the last restart.
Should you turn off validate_timestamps on WordPress?
With opcache.validate_timestamps=0, PHP never checks whether a file changed. That saves a little work per request, and it’s common advice. On WordPress it has a catch: core, plugin and theme updates from wp-admin or auto-updates write new files to disk, and PHP keeps running the old bytecode until the cache is reset. You can end up with half-updated code in memory.
I only turn it off when deployments go through a pipeline that resets OPcache afterwards, either by reloading PHP-FPM or by calling opcache_reset() from a web request, and when updates from wp-admin are disabled. Otherwise I leave it on and, if needed, raise revalidate_freq.
JIT, available since PHP 8.0, is a separate question. It speeds up CPU-heavy code, and a typical WordPress request spends much of its time waiting on the database and on I/O. If you want to try it, measure TTFB on uncached requests with and without it before you keep it.
How many PHP-FPM workers does WordPress need?
There’s no universal number. The ceiling comes from memory, and the useful value comes from traffic and CPU. The pool settings live in the pool file, often /etc/php/<version>/fpm/pool.d/www.conf on Debian and Ubuntu:
pmpicks the mode.statickeeps a fixed number of workers.dynamickeeps a range between the spare server limits.ondemandstarts workers only when requests arrive and stops them afterpm.process_idle_timeout, which saves memory on quiet sites and adds startup time on the first request.pm.max_childrenis the hard limit on simultaneous workers, and so on simultaneous PHP requests.pm.start_servers,pm.min_spare_serversandpm.max_spare_serversshape thedynamicmode.pm.max_requestsrecycles a worker after that many requests. The default of 0 never recycles. A finite value contains slow memory leaks from plugins.
To set pm.max_children, take the RAM you can give PHP after the database, Redis, the web server and the OS, and divide it by the real average memory of a worker under normal traffic. The process name changes between distributions and PHP versions:
ps -C php-fpm8.3 -o rss= | awk '{ s += $1; n++ } END { printf "%d workers, %.0f MB average\n", n, s / n / 1024 }'Use the measured average, not memory_limit. The limit is a cap for one runaway request; most workers stay well below it. Leave headroom for peaks, and check that the result is sane for your CPU too. When requests are CPU-bound, workers far beyond the number of cores just wait for each other.
How do you see whether PHP-FPM is the bottleneck?
PHP-FPM tells you, if you ask. Two signals are enough for most diagnoses:
- The FPM log line
server reached pm.max_children setting. If it appears at peak times, requests queued. - The status page, enabled with
pm.status_path. The fieldslisten queue,max listen queue,max children reachedandslow requeststell you whether requests waited and how often.
The slow log is the most useful setting most servers don’t have. It writes a PHP backtrace for every request that runs longer than a threshold, so you can see which plugin function was executing:
; pool file, for example www.conf
pm.status_path = /fpm-status
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/www-slow.log
pm.max_requests = 500Those values are examples to adjust, not recommendations. Pick a slowlog threshold that catches the requests you care about without filling the disk, and restrict the status path to localhost or your monitoring. Reload PHP-FPM after changes (for example systemctl reload php8.3-fpm, adjusting the service name).
If the slow log keeps pointing at database calls, the PHP layer is fine and the work moves to wp_options autoload and database optimization. If it points at HTTP calls to external APIs, cache those responses or move them to cron.
What about the PHP version?
Run a PHP version that still receives security support and that your plugins support. Newer major versions have generally made WordPress faster, and old ones stop getting fixes. Check compatibility on staging first, since old plugins are what usually block an upgrade. Site Health warns you when your PHP version is outdated.
For where PHP sits next to page cache, object cache and the CDN, see which cache layer you actually need. Tuning PHP makes cache misses cheaper. It doesn’t replace caching.
Frequently asked questions
Can I tune OPcache on shared hosting?
Usually not. OPcache memory is set for the whole server, and many shared hosts don’t let you change it. You can still check that it’s enabled with phpinfo() or in the host’s PHP settings. If it’s off, that’s a reason to change hosts.
Why is my site slow only under traffic?
That pattern points at the worker pool or the database, not at OPcache. Check the FPM log for the pm.max_children warning and the status page for a listen queue during the slow period.
Should I just set pm to static with a high max_children?
Only if the memory math supports it. Too many workers on too little RAM pushes the server into swap, and everything gets slower at once. Measure worker memory first.
If your server response time is high and you want someone to trace it through PHP, OPcache, the database and the cache layers with real measurements, start with my WordPress performance audit.