WordPress Server Tuning: PHP-FPM, OPcache, Redis and MySQL
How to tune a WordPress server: sizing PHP-FPM workers, OPcache memory, a Redis object cache and MySQL or MariaDB settings, with safe starting points.
WordPress server tuning means adjusting the software under WordPress, such as PHP and the database, so it serves pages quickly and stays stable under load. The four settings that matter most are the PHP-FPM worker pool, OPcache, a persistent object cache such as Redis, and the database server’s memory. This guide explains what each one does, how to size it for your server, and the mistakes we see most often.
One warning before we start: every number below is a starting point, not a recommendation for your site. The right values depend on your server’s memory, your plugins and your traffic. Measure, change one thing at a time, and test on staging first.
Before you tune: measure
Tuning without numbers is guessing. Before you change any setting, collect:
- Server memory and CPU use during busy and quiet hours.
- Response times for key uncached pages, such as the cart, checkout, account pages and the admin dashboard.
- PHP-FPM status: how many workers are active, idle, and how often requests had to wait.
- Slow requests and slow queries from the PHP-FPM slow log and the database slow query log.
If you haven’t yet confirmed the server is the bottleneck, start with our diagnostic guide, why is my WordPress site slow. Many slow sites are slow because of front-end code, and no server tuning will fix that.
PHP-FPM: sizing the worker pool
PHP-FPM (FastCGI Process Manager) runs a pool of PHP worker processes. Each worker handles one request at a time. If all workers are busy, new requests wait in a queue. If you allow too many workers, the server runs out of memory and starts swapping, which is far worse.
The process manager modes
The pm directive controls how workers are managed. The PHP-FPM configuration reference on php.net lists three options:
static: a fixed number of workers, set bypm.max_children. Predictable, and a good fit for a dedicated server running one site.dynamic: the number of workers moves between limits set bypm.min_spare_servers,pm.max_spare_serversandpm.max_children.ondemand: workers start only when requests arrive and stop afterpm.process_idle_timeout. Useful on servers hosting many small, low-traffic sites.
Calculating pm.max_children
pm.max_children is the most important setting. It caps the number of simultaneous PHP requests. A simple way to size it:
- Find how much memory one PHP-FPM worker uses on your site under normal load. Tools like
psortopshow the resident memory of eachphp-fpmprocess. WordPress workers with many plugins often use far more than a fresh install. - Decide how much memory you can give PHP after reserving room for the operating system, the web server, the database and Redis.
- Divide the PHP memory by the average worker size, then round down and leave some headroom.
For example, if you can give PHP 4 GB and each worker uses about 100 MB, you could support roughly 40 workers. But measure your own numbers. Memory per worker varies a lot between sites.
A starting point for a dedicated server might look like this:
; /etc/php/8.3/fpm/pool.d/www.conf (path varies by distribution and PHP version)
pm = dynamic
pm.max_children = 40
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15
pm.max_requests = 500
; Log requests that take longer than 5 seconds, with a PHP backtrace
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/www-slow.log
pm.max_requests restarts each worker after a set number of requests. The PHP manual notes this helps work around memory leaks in third-party code. The default is 0, which means workers never restart.
The slow log is one of the most useful tools you have. When a request takes longer than request_slowlog_timeout, PHP-FPM writes a backtrace showing which function was running, often pointing straight to the slow plugin.
Signs your pool is wrong
- Too small: the PHP-FPM log warns that the server reached
pm.max_children, and pages are slow at busy times while the CPU is mostly idle. - Too large: memory runs out, the server swaps, or the operating system kills processes, including the database.
OPcache: keeping compiled code in memory
Every time PHP runs a file, it first compiles it into bytecode. OPcache stores that compiled code in shared memory so PHP can skip the compile step. WordPress plus a typical set of plugins loads thousands of PHP files, so this matters.
OPcache is enabled by default in modern PHP builds, but the default sizes can be too small for large sites. The key settings, from the OPcache configuration reference:
| Setting | Default | What it does |
|---|---|---|
opcache.memory_consumption | 128 (MB) | Shared memory for compiled code |
opcache.interned_strings_buffer | 8 (MB) | Memory for repeated strings |
opcache.max_accelerated_files | 10000 | Maximum number of cached scripts |
opcache.validate_timestamps | 1 | Check whether files changed on disk |
opcache.revalidate_freq | 2 (seconds) | How often to check for changed files |
A starting point for a server running a large WordPress site with many plugins:
; Starting point only. Check opcache_get_status() before and after.
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=60
Note that PHP rounds opcache.max_accelerated_files up to the next number in a fixed list of primes, so the real limit may be a little higher than what you set.
How to tell if OPcache is too small: call opcache_get_status() from a protected admin script, or use a monitoring plugin, and check memory use, the number of cached scripts, and whether the cache is full. A full cache means PHP starts compiling files again on every request.
About validate_timestamps
Setting opcache.validate_timestamps=0 stops PHP from checking files for changes, which saves a little time. But then PHP keeps running old code until OPcache is reset. That includes plugin and core updates made from the WordPress dashboard. Only turn it off if every code change goes through a deployment process that clears the cache. A longer revalidate_freq is a safer middle ground.
What about the JIT?
PHP 8 added a JIT compiler. For typical WordPress workloads, which spend much of their time waiting on the database and building strings, the JIT usually makes little difference. Note that defaults changed in PHP 8.4, so check the manual for your version before enabling it.
Redis: a persistent object cache
WordPress has a built-in object cache, but by default it only lasts for a single request. A persistent object cache stores those results in memory between requests, so repeated queries for options, posts, users and metadata don’t hit the database every time.
To use Redis with WordPress, you need:
- A Redis server, ideally on the same machine or private network.
- The PHP Redis extension or a compatible client library.
- An
object-cache.phpdrop-in inwp-content, usually installed by a plugin such as Redis Object Cache.
Two Redis settings matter most for WordPress:
# /etc/redis/redis.conf (starting point; size to your data)
maxmemory 256mb
maxmemory-policy allkeys-lru
maxmemory caps how much memory Redis can use. allkeys-lru tells Redis to drop the least recently used keys when it is full, which is what you want for a cache. Without a limit, Redis can grow until it competes with PHP and the database for memory.
When Redis helps, and when it doesn’t
Redis helps most on pages that can’t be served from a page cache: WooCommerce carts and checkouts, membership areas, LMS courses, and the admin dashboard. It also reduces database load on busy sites.
It won’t fix a slow query that runs once per page, and it can’t help if a plugin bypasses the WordPress cache functions. If your site has a lot of autoloaded options, fix that first. Our guide to cleaning up a bloated WordPress database explains how.
MySQL and MariaDB tuning
WordPress stores everything in MySQL or MariaDB. WordPress recommends MySQL 8.0 or greater, or MariaDB 10.11 or greater, on its requirements page.
innodb_buffer_pool_size
The InnoDB buffer pool is the database’s main memory cache for table data and indexes. If your data fits in it, most reads come from memory instead of disk. This is the single most important database setting.
# /etc/mysql/my.cnf or a file in conf.d (path varies)
[mysqld]
innodb_buffer_pool_size = 2G
Size it based on your actual database size and the memory left after PHP, the web server and Redis. On a server dedicated only to the database, it can take most of the memory. On a single server running everything, it has to share. In current MySQL versions the buffer pool can be resized while the server runs, as described in the MySQL buffer pool resizing documentation.
The slow query log
Turn on the slow query log to find the queries worth fixing:
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
The default long_query_time in MySQL is 10 seconds, which is far too high to catch WordPress problems. Start at 1 second, then lower it if the log stays quiet. Tools like mysqldumpslow summarize the log. Most slow WordPress queries come from meta queries on large wp_postmeta tables, search, and plugins that run unindexed queries.
Putting it together: memory budget
On a single server, everything competes for the same memory. Plan it explicitly. For example, on a server with 8 GB of memory, you might reserve space for the operating system, give the database buffer pool a fixed share, cap Redis, and let PHP-FPM use what’s left. Then calculate pm.max_children from that remainder, not from the total.
When you change any of these settings:
- Change one setting at a time.
- Test on staging under realistic load.
- Watch memory, response times and error logs for a few days in production.
- Write down what you changed and why.
Keep PHP current, too. Newer PHP releases are generally faster and still receive security fixes. See our guide to upgrading PHP on WordPress safely.
When server tuning isn’t the answer
If you are on shared hosting or a managed platform, you may not have access to any of these settings. That is not always a bad thing, as long as the host tunes them well. Our comparison of managed WordPress hosting vs a VPS helps you decide which fits. And if the slow part is the browser, read our Core Web Vitals guide for WordPress instead.
Need a second pair of eyes?
We tune WordPress servers based on measured data from your site, not copied configs. Our server tuning service covers PHP-FPM, OPcache, object caching and the database, with before-and-after numbers. Contact us to tell us about your setup.
Frequently asked questions
- How many PHP-FPM workers does a WordPress site need?
- It depends on server memory and how much memory each PHP process uses. Divide the memory you can give to PHP by the average size of one process, then leave headroom. Measure the process size on your own server rather than copying a number.
- Does WordPress need Redis?
- Not always. A persistent object cache like Redis helps most on sites with many uncached pages, such as WooCommerce stores, membership sites and busy admin areas. A small brochure site behind a page cache may see little difference.
- Should I turn off opcache.validate_timestamps?
- Only if you have a deployment process that clears OPcache after every code change. With it turned off, PHP keeps running the old code until the cache is reset, including after plugin updates from the dashboard.
- What is a good innodb_buffer_pool_size for WordPress?
- Ideally large enough to hold your working data set in memory. On a server shared with PHP and a web server, it has to fit alongside them, so size it from your actual database size and available memory.