When I first started managing WordPress sites on VPS servers, I made the mistake of leaving PHP at
default settings. The sites worked, pages loaded, but performance was mediocre at best. Then I
discovered that a few targeted PHP configuration changes could cut page generation time in
half—without touching WordPress code, without adding caching plugins, just by telling PHP how to
work more efficiently.
PHP 8.3 is the fastest PHP version to date, with substantial improvements over even PHP 8.0 and 8.1.
But that speed potential isn’t fully realized with default configurations designed for broad
compatibility rather than optimal performance. On a VPS where you control the server environment,
you can tune PHP specifically for WordPress workloads.
This guide covers the PHP tuning I apply to every WordPress VPS I manage. We’ll start with
understanding where PHP spends time during WordPress requests, then move through OPcache
configuration (the single biggest performance win), JIT compilation, PHP-FPM pool tuning, and memory
management. By the end, you’ll have a tested configuration that significantly improves WordPress
performance.
Understanding PHP Performance in WordPress
Before changing settings, it’s valuable to understand what PHP actually does when WordPress handles a
request. This understanding helps you focus tuning efforts where they’ll have the most impact.
The PHP Request Lifecycle
Every WordPress page request triggers a sequence of PHP operations. First, PHP reads the PHP source
files from disk—WordPress core, your theme, all active plugins. Then PHP parses these files and
compiles them to opcodes, an intermediate representation that the PHP engine can execute. Finally,
PHP executes those opcodes, running WordPress core initialization, loading plugins, querying the
database, processing templates, and building the HTML response.
Without caching, this compilation phase happens on every single request. WordPress core alone has
hundreds of PHP files. A typical plugin stack might add hundreds more. Parsing and compiling all
these files takes meaningful time—often 50-100 milliseconds or more depending on server speed. This
is pure overhead that produces the same result every time (assuming code hasn’t changed).
The execution phase is where WordPress actually does its work: loading settings, checking user
authentication, running database queries, processing shortcodes, rendering templates. This phase’s
duration depends on page complexity, plugin efficiency, and database performance. It’s often the
largest component of request time, but we’ll address it separately through database and application
optimization.
Where Tuning Makes a Difference
PHP tuning primarily affects the compilation and execution phases. OPcache eliminates compilation
overhead by storing compiled opcodes between requests—instead of parsing and compiling files every
time, PHP retrieves pre-compiled code from memory. This is the single most impactful PHP
optimization, easily providing 30-50% reduction in PHP processing time for WordPress.
JIT (Just-In-Time) compilation goes further, compiling hot code paths to native machine code. The
benefit for typical WordPress requests is more modest—perhaps 5-15%—but it’s essentially free
performance once configured.
PHP-FPM process management determines how many concurrent requests your server can handle
efficiently. Too few processes and requests queue during traffic spikes. Too many and your server
runs out of memory and starts swapping, killing performance for everyone.
Memory settings ensure PHP has enough resources to handle complex operations (like loading large
plugins or processing uploads) without hitting limits that cause failures.
Finding and Editing PHP Configuration
PHP configuration is spread across multiple files. Understanding the structure prevents confusion
when making changes.
Configuration File Locations
The main PHP configuration file is php.ini. Its location varies by operating system and how PHP was
installed. On Ubuntu/Debian systems using PHP-FPM (the standard for WordPress hosting), the path is
typically /etc/php/8.3/fpm/php.ini. On CentOS/RHEL systems, look in /etc/php.ini or /etc/php.d/.
To find your php.ini definitively, create a PHP file containing phpinfo() and view it in your
browser—the loaded configuration file is shown at the top. Alternatively, run php –ini from the
command line to see which configuration files are loaded for CLI PHP (note that FPM may load
different files).
PHP also loads configuration from a conf.d directory—on Ubuntu/Debian, this is
/etc/php/8.3/fpm/conf.d/. Files in this directory are loaded in alphabetical order and can override
php.ini settings. OPcache and other extensions often have their own files here.
PHP-FPM has separate pool configuration, typically in /etc/php/8.3/fpm/pool.d/www.conf. This controls
process management, not PHP behavior per se, but it’s critical for performance.
Making and Applying Changes
Edit configuration files with your favorite text editor (nano, vim, etc.) as root. After making
changes, restart PHP-FPM for them to take effect. On Ubuntu/Debian, the command is sudo systemctl
restart php8.3-fpm. On CentOS/RHEL, it might be sudo systemctl restart php-fpm.
Always verify changes took effect. Create a temporary PHP file that outputs phpinfo() and check the
relevant settings. Alternatively, use WordPress’s Site Health tool, which shows PHP configuration
information.
Before making significant changes, document your original values. A comment in the config file noting
the original value makes reverting easy if something goes wrong.
OPcache Configuration
OPcache is the most important PHP optimization for WordPress. It stores compiled opcodes in shared
memory, eliminating the parsing and compilation overhead that otherwise occurs on every request.
How OPcache Transforms Performance
Without OPcache, PHP reads every requested PHP file from disk, tokenizes it, parses it into an
abstract syntax tree, and compiles it to executable opcodes—on every request. A WordPress page load
involving hundreds of PHP files repeats this work for each file, even though the code hasn’t changed
since the last request.
OPcache stores the compiled opcodes in shared memory. After the first request compiles a file,
subsequent requests retrieve the cached opcodes directly. No disk reading, no parsing, no
compilation—just immediate execution of pre-compiled code.
The performance difference is substantial. Time spent parsing and compiling typically drops to near
zero, representing a 20-40% reduction in total PHP processing time for a typical WordPress page. For
pages with many included files or complex plugins, the improvement can be even larger.
Essential OPcache Settings
OPcache is usually enabled by default in PHP 8.x, but default settings aren’t optimized for
WordPress. Here are the settings I configure on WordPress VPS servers:
opcache.enable=1 ensures OPcache is active. This should already be 1 by default, but verify.
opcache.memory_consumption=256 sets the memory pool for storing cached opcodes, in megabytes. The
default is often just 128MB. WordPress with a typical plugin stack easily needs 150-200MB of cache
space. Setting 256MB provides headroom and prevents cache eviction that forces recompilation. Check
OPcache status to see actual usage—if you’re consistently near the limit, increase this.
opcache.interned_strings_buffer=16 allocates memory for storing interned strings (strings that appear
repeatedly in code like function names and class names). 16MB is generous for WordPress; the default
8MB is often insufficient.
opcache.max_accelerated_files=10000 sets the maximum number of files OPcache can store. A WordPress
installation with multiple plugins can have 5000+ PHP files. The default of 2000 would cause cache
eviction. Set this to 10000 or higher for complex plugin stacks. Note this must be a prime number in
older PHP versions; in PHP 8.x, any number works.
opcache.validate_timestamps=0 is a crucial production setting. When set to 0, OPcache never checks if
source files have been modified—it assumes cached code is valid indefinitely. This eliminates stat()
calls that check file modification times, providing a small but meaningful performance boost.
The trade-off is that OPcache won’t automatically pick up code changes. After updating WordPress,
themes, or plugins, you must manually clear the cache by restarting PHP-FPM. For production sites
where code changes are infrequent and deliberate, this trade-off is worthwhile. For development
environments where you’re actively editing code, set this to 1.
opcache.revalidate_freq is only relevant if validate_timestamps=1. It specifies how many seconds
between timestamp checks. For development, 0 (check every request) makes sense. For production with
timestamps enabled, 60 (check once per minute) reduces overhead while still eventually detecting
changes.
Verifying OPcache Is Working
Create a PHP file that displays OPcache status to verify it’s working effectively. The
opcache_get_status() function returns detailed information including memory usage, cache hit rates,
and cached file counts.
Key metrics to check: cache hits should far exceed misses after the cache warms up. Hit rate of 95%+
indicates effective caching. Memory usage should have headroom—if used_memory approaches total
memory_consumption, you need more memory. If cache_full is true even temporarily, scripts are being
evicted and recompiled.
Several OPcache GUI tools are available that visualize this data nicely. Install one temporarily to
spot issues, then remove it for security.
JIT Compilation
PHP 8.0 introduced JIT compilation, which takes OPcache further by compiling opcodes to native
machine code. For WordPress workloads, gains are more modest than OPcache alone, but JIT is
essentially free performance if you have memory to spare.
How JIT Differs from OPcache
OPcache caches PHP opcodes—an intermediate representation that the PHP engine interprets. JIT
compiles hot code paths further, generating native machine code that the CPU executes directly
without interpretation overhead. Think of OPcache as eliminating parsing and JIT as eliminating
interpretation.
For compute-heavy code (math operations, complex algorithms), JIT provides substantial speedups—50%
or more is common in benchmarks. For typical WordPress requests that are heavily I/O-bound (waiting
for database, waiting for file reads), JIT’s benefit is more modest because less time is spent in
CPU-bound execution.
In real-world WordPress testing, I typically see 5-15% improvement in PHP processing time from JIT.
Not dramatic, but measurable and essentially free once configured.
JIT Configuration
JIT requires OPcache to be enabled first. Then configure these settings:
opcache.jit=1255 enables JIT with the tracing mode optimized for general workloads. The four-digit
value configures JIT behavior; 1255 is the commonly recommended setting for WordPress:
The first digit (1) enables tracing JIT. The second digit (2) configures optimization level. The
third digit (5) sets trigger mode—compile after functions are called 5 times. The fourth digit (5)
sets optimization intensity.
Other common values include 1205 (less aggressive, uses less CPU for compilation) or disable (0) for
debugging environments where JIT can interfere with step debuggers.
opcache.jit_buffer_size=128M allocates memory for storing JIT-compiled native code. 128MB is generous
for most WordPress sites. If you’re memory-constrained, 64MB is usually sufficient. Check JIT status
to see actual buffer utilization.
JIT Caveats
JIT works at the PHP execution layer but doesn’t optimize I/O-bound code. If your site is slow due to
database queries or external API calls, JIT won’t help those bottlenecks.
JIT is incompatible with some debugging tools. Xdebug, for example, disables JIT when active. This is
fine—you don’t want JIT on development environments anyway.
Some plugins have reported edge-case compatibility issues with JIT. These are rare in 2024/2025, but
if you experience unexplained behavior after enabling JIT, disabling it to test is straightforward.
PHP-FPM Pool Configuration
PHP-FPM manages worker processes that handle incoming requests. Pool configuration determines how
many requests your server can handle concurrently and how memory is allocated across workers.
Process Manager Modes
PHP-FPM’s pm setting controls how worker processes are created and managed. Three modes are
available:
pm = dynamic creates processes as needed within defined limits. This is usually best for WordPress
sites with variable traffic—workers spawn during busy periods and die during quiet periods,
conserving memory.
pm = static maintains a fixed number of workers constantly. Best for high-traffic sites with
consistent load where startup time for new workers would cause latency during spikes.
pm = ondemand only spawns workers when requests arrive, killing them when idle. Very memory-efficient
for low-traffic sites but adds latency for the first request after idle periods.
For most WordPress sites, dynamic mode provides the best balance. Use static if you know your traffic
is consistently high and you want to eliminate any process spawning latency.
Calculating pm.max_children
pm.max_children limits the maximum number of concurrent PHP processes. This is the most critical
PHP-FPM setting for performance—get it wrong and you either run out of memory (too high) or can’t
handle concurrent traffic (too low).
Each PHP-FPM worker consumes memory. Typical WordPress sites with a handful of plugins use 50-100MB
per worker. Heavy WooCommerce sites or those with many plugins might use 150MB or more. First,
measure your actual usage:
Run ps aux –sort=rss | grep php-fpm after your site has been active to see memory per worker
process. The RSS (Resident Set Size) column shows memory in KB. Average across workers and convert
to MB.
Then calculate max_children based on available memory. Determine how much RAM is available for PHP
after accounting for other services (MySQL typically needs 30-50% of RAM on a WordPress server,
Nginx needs minimal memory). Divide available PHP memory by average worker size.
For example, a 4GB VPS might allocate 2GB for MySQL, 100MB for Nginx and system overhead, leaving
roughly 1.8GB for PHP. With 90MB workers, that’s 1800/90 = 20 max_children.
Conservative is better than aggressive. If you set max_children too high and traffic spike causes all
workers to spawn, the server runs out of RAM and starts swapping. Swapping is catastrophically slow
for PHP workloads and can make the site effectively unusable. Set max_children so that even at
maximum, your server has memory headroom.
Other Pool Settings
For dynamic mode, several other settings matter:
pm.start_servers is the number of workers to start initially. Usually 2-4. This has minimal ongoing
impact—FPM scales up as needed.
pm.min_spare_servers is the minimum number of idle workers to maintain. Keeping 1-2 idle workers
available means incoming requests don’t wait for worker spawning.
pm.max_spare_servers is the maximum idle workers before FPM kills extras. Set this slightly above
min_spare_servers—maybe 3-4—to prevent constant spawning and killing.
pm.max_requests limits how many requests a single worker handles before being recycled. This protects
against memory leaks in poorly-written plugins. 500-1000 is a reasonable value. Don’t set this too
low (constant worker recycling adds overhead) or too high (leaked memory accumulates).
Memory and Execution Settings
Core php.ini settings control resource limits for individual scripts. These need to accommodate
WordPress’s requirements without allowing runaway processes.
Memory Limits
memory_limit sets the maximum memory a single PHP request can allocate. WordPress recommends 256MB as
minimum. For complex sites with page builders, large plugins, or image manipulation, 512MB may be
needed.
Setting this too low causes out-of-memory errors during legitimate operations. Setting it too high
(like 1GB) allows misbehaving scripts to consume excessive resources. 256MB is appropriate for most
sites; increase only if you encounter legitimate memory limit errors.
Remember this is per-request, not total PHP memory. If you have 20 concurrent workers each
potentially using 256MB, theoretical maximum PHP memory is 5GB—far more than most VPS servers have.
In practice, average memory per request is much lower than the limit; the limit is a safety cap.
Execution Time Limits
max_execution_time limits how long a single script can run before PHP terminates it. Default is often
30 seconds. For most WordPress front-end requests, this is generous—pages should generate in under a
second.
Longer limits are needed for certain operations: bulk imports, large plugin operations, backup
generation. Some plugins temporarily increase this limit for specific operations via ini_set(). I
generally leave the default at 30-60 seconds unless specific functionality requires more.
max_input_time limits time spent parsing request input (POST data, file uploads). 60 seconds is
usually sufficient. Increase for large file uploads that take longer to transfer.
Input and Upload Settings
max_input_vars limits the maximum number of variables in a request. Default is often 1000, which can
be insufficient for WordPress admin pages with many custom fields, complex WooCommerce
configurations, or large forms. Increase to 3000-5000 to avoid silent form truncation.
upload_max_filesize limits individual file upload sizes. WordPress media uploads are limited by this.
For publisher sites with large images or video, 64MB or more is appropriate. Remember to also adjust
post_max_size, which limits total POST data size and must be larger than upload_max_filesize.
Putting It All Together: Complete Configuration
Based on managing hundreds of WordPress sites on VPS servers, here’s the configuration I use as a
starting point. Adjust based on your specific server size and traffic patterns.
For a 2GB VPS (Entry-Level)
This configuration assumes approximately 1GB available for PHP after MySQL and system overhead:
In php.ini or a conf.d file: memory_limit = 256M, max_execution_time = 60, max_input_time = 60,
max_input_vars = 5000, upload_max_filesize = 64M, post_max_size = 128M. For OPcache: opcache.enable
= 1, opcache.memory_consumption = 192, opcache.interned_strings_buffer = 12,
opcache.max_accelerated_files = 10000, opcache.validate_timestamps = 0, opcache.jit = 1255,
opcache.jit_buffer_size = 64M.
In pool.d/www.conf: pm = dynamic, pm.max_children = 10, pm.start_servers = 2, pm.min_spare_servers =
1, pm.max_spare_servers = 3, pm.max_requests = 500.
For a 4GB VPS (Mid-Range)
With more memory available, allocate more to caching and allow more concurrent workers:
OPcache settings: opcache.memory_consumption = 256, opcache.interned_strings_buffer = 16,
opcache.jit_buffer_size = 128M.
Pool settings: pm.max_children = 20, pm.start_servers = 3, pm.min_spare_servers = 2,
pm.max_spare_servers = 5.
For a 8GB+ VPS (Production)
Larger allocations for caching and workers: opcache.memory_consumption = 384, opcache.jit_buffer_size
= 256M, pm.max_children = 40, pm.start_servers = 5.
Monitoring and Ongoing Tuning
Initial configuration is just the beginning. Monitoring actual performance helps identify whether
tuning is effective and where further adjustment might help.
Key Metrics to Track
OPcache hit rate should be 95%+ after warmup. Low hit rates indicate cache eviction—increase
memory_consumption or max_accelerated_files. The cache_full indicator should never be true; if it
is, allocate more memory.
PHP-FPM active processes during traffic peaks shows whether you have enough workers. If max_children
is consistently reached, you either need more memory/workers or should investigate why requests are
taking long enough to accumulate.
Memory usage overall—monitor server RAM usage to ensure PHP isn’t pushing other services into swap.
If swap usage increases during traffic, reduce max_children or add RAM.
PHP processing time (via Query Monitor, New Relic, or similar) should decrease after tuning. Baseline
before changes, then measure after. Expect 20-40% improvement from OPcache optimization on a
previously untuned server.
After Updates
With validate_timestamps=0, OPcache doesn’t detect code changes. After updating WordPress core,
themes, or plugins, clear the cache by restarting PHP-FPM: sudo systemctl restart php8.3-fpm.
Forgetting this means running outdated cached code—updates don’t take effect until cache clears.
Some managed hosting control panels and deployment tools handle this automatically. If managing via
SSH, add cache clearing to your update workflow.
Conclusion
PHP tuning provides substantial WordPress performance improvements without changing application code.
OPcache alone typically reduces PHP processing time by 30-50%. JIT adds another 5-15%. Proper
PHP-FPM configuration ensures your server handles traffic efficiently without resource exhaustion.
Start with the configurations I’ve outlined, adjusted for your server’s memory. Monitor OPcache
status, FPM processes, and overall memory to refine settings for your specific workload. Remember to
clear cache after code updates when running with timestamp validation disabled.
These optimizations apply to the PHP layer only. For complete WordPress performance, you’ll also want
database optimization, page caching, and front-end optimization. But PHP tuning establishes a solid
foundation—every request benefits, whether cached or uncached, logged-in or anonymous.
The time invested in proper PHP configuration pays dividends on every page load. Configure once,
benefit forever.
admin
Tech enthusiast and content creator.