A default PHP installation on Ubuntu handles perhaps 10-20 requests per second before response times start climbing. With proper tuning of PHP-FPM, OPcache, and JIT compilation, the same hardware can serve 200-500 requests per second — a 10-25x improvement without writing a single line of application code. This guide covers every PHP performance lever available on Ubuntu 24.04, with concrete configuration values based on your VPS resources, real benchmarks, and the reasoning behind each setting so you can adapt them to your workload.
MassiveGRID Ubuntu VPS includes: Ubuntu 24.04 LTS pre-installed · Proxmox HA cluster with automatic failover · Ceph 3x replicated NVMe storage · Independent CPU/RAM/storage scaling · 12 Tbps DDoS protection · 4 global datacenter locations · 100% uptime SLA · 24/7 human support rated 9.5/10
Deploy a self-managed VPS — from $1.99/mo
Need dedicated resources? — from $19.80/mo
Want fully managed hosting? — we handle everything
How PHP-FPM, OPcache, and JIT Fit Together
Think of PHP performance as three layers, each building on the one below:
| Layer | What It Does | Impact |
|---|---|---|
| PHP-FPM | Manages a pool of PHP worker processes that handle requests | Controls concurrency — how many requests can be processed simultaneously |
| OPcache | Caches compiled PHP bytecode in shared memory | Eliminates re-parsing and re-compiling PHP files on every request (2-5x speedup) |
| JIT | Compiles frequently-executed bytecode into native machine code | Further speeds up CPU-intensive operations (10-30% on compute-heavy code) |
Without OPcache, every PHP request reads source files from disk, parses them into an abstract syntax tree, compiles them into bytecode, and executes the bytecode. OPcache eliminates steps 1-3 by caching the bytecode in shared memory. JIT goes one step further by converting hot bytecode paths into native machine instructions.
Prerequisites
This guide assumes you have:
- An Ubuntu 24.04 VPS — deploy one here
- A LEMP stack installed (Nginx + MySQL/MariaDB + PHP 8.3) — follow our LEMP stack guide
- SSH access with sudo privileges — see our initial setup guide
Verify your PHP version:
php -v
Expected output:
PHP 8.3.6 (cli) (built: Apr 15 2024 19:21:47) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.6, Copyright (c) Zend Technologies
with Zend OPcache v8.3.6, Copyright (c), by Zend Technologies
Verify PHP-FPM is running:
sudo systemctl status php8.3-fpm
PHP-FPM Pool Configuration
The PHP-FPM pool configuration controls how many PHP processes run, how they are managed, and how they respond to traffic patterns. The default configuration is conservative and designed for shared hosting — not for a VPS where you control all resources.
Edit the pool configuration:
sudo nano /etc/php/8.3/fpm/pool.d/www.conf
Process Manager Mode
PHP-FPM supports three process manager modes:
| Mode | Behavior | Best For |
|---|---|---|
static |
Fixed number of workers, always running | Consistent traffic, dedicated servers |
dynamic |
Workers scale between min and max based on demand | Variable traffic, most VPS workloads |
ondemand |
Workers spawn only when needed, die after idle timeout | Low-traffic sites, memory-constrained environments |
For most VPS workloads, dynamic is the right choice. It balances memory usage with responsiveness:
pm = dynamic
The max_children Formula
The pm.max_children setting is the single most important PHP-FPM parameter. Set it too low and requests queue up. Set it too high and your VPS runs out of memory and starts swapping, which destroys performance.
The formula:
max_children = (Total RAM - RAM for OS/Nginx/DB) / Average PHP Worker Memory
To find your average PHP worker memory:
ps -eo pid,rss,comm | grep php-fpm | awk '{sum+=$2; count++} END {print "Average:", sum/count/1024, "MB", "| Workers:", count}'
Typical values by application type:
| Application | Average Worker Memory |
|---|---|
| Simple PHP site | 20-30 MB |
| Laravel / Symfony | 40-60 MB |
| WordPress (no heavy plugins) | 40-60 MB |
| WordPress (WooCommerce, page builders) | 60-100 MB |
| Magento / heavy e-commerce | 80-150 MB |
On a MassiveGRID Cloud VPS with 4GB RAM: reserve 1GB for OS, Nginx, and MySQL, leaving 3GB for PHP-FPM. At 50MB per worker, that gives approximately 60 max_children. Here are recommended values by VPS size:
| VPS RAM | Available for PHP | At 30MB/worker | At 50MB/worker | At 80MB/worker |
|---|---|---|---|---|
| 1 GB | ~512 MB | 17 | 10 | 6 |
| 2 GB | ~1.2 GB | 40 | 24 | 15 |
| 4 GB | ~3 GB | 100 | 60 | 37 |
| 8 GB | ~6.5 GB | 216 | 130 | 81 |
| 16 GB | ~14 GB | 466 | 280 | 175 |
Complete Dynamic Pool Configuration
For a 4GB VPS running a WordPress or Laravel application (approximately 50MB per worker):
; Process manager
pm = dynamic
pm.max_children = 60
pm.start_servers = 15
pm.min_spare_servers = 10
pm.max_spare_servers = 25
pm.max_requests = 500
pm.process_idle_timeout = 10s
What each setting does:
pm.max_children = 60— absolute maximum worker processes (calculated above)pm.start_servers = 15— workers to start immediately (usually max_children / 4)pm.min_spare_servers = 10— minimum idle workers kept readypm.max_spare_servers = 25— maximum idle workers (excess are killed)pm.max_requests = 500— recycle each worker after 500 requests to prevent memory leakspm.process_idle_timeout = 10s— how long an idle worker survives before being killed
User and Group Settings
Ensure the pool runs as the correct user (matching your Nginx and file ownership):
user = www-data
group = www-data
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
OPcache Configuration
OPcache is included with PHP 8.x but needs tuning beyond its defaults. Edit the OPcache configuration:
sudo nano /etc/php/8.3/fpm/conf.d/10-opcache.ini
Replace the contents with:
[opcache]
; Enable OPcache
opcache.enable=1
opcache.enable_cli=0
; Memory settings
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=20000
; Revalidation settings (production)
opcache.validate_timestamps=0
opcache.revalidate_freq=0
; Optimization level
opcache.optimization_level=0x7FFEBFFF
; Error handling
opcache.save_comments=1
opcache.enable_file_override=1
; Preloading (PHP 7.4+)
; opcache.preload=/var/www/html/preload.php
; opcache.preload_user=www-data
Let's break down the critical settings:
opcache.memory_consumption
How much shared memory (in MB) OPcache uses to store compiled scripts. The default of 128MB is often too low for larger applications:
# Check current OPcache memory usage
php -r "var_dump(opcache_get_status()['memory_usage']);"
Rule of thumb: set it to 2x your current usage. A WordPress site with plugins needs 128-192MB. A large Laravel application needs 256-512MB.
opcache.max_accelerated_files
Maximum number of PHP files that can be cached. Count your project files:
find /var/www -name "*.php" | wc -l
Set this to the nearest prime number above your count. Common values: 10000 (small sites), 20000 (WordPress with plugins), 50000 (large frameworks). The internal hash table uses prime numbers, so PHP rounds up to the nearest prime anyway.
opcache.validate_timestamps
This is the single biggest OPcache performance lever:
validate_timestamps=1(default): PHP checks if source files have changed on every request. Safe but slower.validate_timestamps=0(production): PHP never checks source files. Maximum performance, but you must restart PHP-FPM after deploying code changes.
For production, always set this to 0 and restart FPM on deployments:
# After deploying new code:
sudo systemctl reload php8.3-fpm
For development or staging, use:
opcache.validate_timestamps=1
opcache.revalidate_freq=2
Verifying OPcache Is Working
Create a temporary info page:
sudo nano /var/www/html/opcache-info.php
<?php
$status = opcache_get_status();
$config = opcache_get_configuration();
echo "<h2>OPcache Status</h2>";
echo "<pre>";
echo "Enabled: " . ($status['opcache_enabled'] ? 'Yes' : 'No') . "\n";
echo "Memory Used: " . round($status['memory_usage']['used_memory'] / 1024 / 1024, 2) . " MB\n";
echo "Memory Free: " . round($status['memory_usage']['free_memory'] / 1024 / 1024, 2) . " MB\n";
echo "Hit Rate: " . round($status['opcache_statistics']['opcache_hit_rate'], 2) . "%\n";
echo "Cached Scripts: " . $status['opcache_statistics']['num_cached_scripts'] . "\n";
echo "Cache Misses: " . $status['opcache_statistics']['misses'] . "\n";
echo "</pre>";
Security note: Delete this file after checking. Never leave PHP info pages accessible in production.
sudo rm /var/www/html/opcache-info.php
JIT Compilation (PHP 8.1+)
JIT (Just-In-Time) compilation was introduced in PHP 8.0 and significantly improved in 8.1+. It compiles frequently executed PHP bytecode into native machine code at runtime.
When JIT Helps
- CPU-intensive computations (image processing, data transformation, mathematical operations)
- Long-running scripts (queue workers, daemons)
- Template rendering with complex logic
When JIT Does Not Help Much
- I/O-bound applications (most web apps that spend time waiting for database queries and external APIs)
- Simple CRUD operations
- Applications that are already fast with OPcache alone
For typical web applications (WordPress, Laravel, Symfony), JIT provides a 5-15% improvement. For compute-heavy workloads, improvements can reach 30% or more.
Configuring JIT
Add JIT settings to your OPcache configuration:
sudo nano /etc/php/8.3/fpm/conf.d/10-opcache.ini
Add these lines:
; JIT Configuration
opcache.jit=1255
opcache.jit_buffer_size=128M
The opcache.jit value is a 4-digit number (CRTO) where each digit controls a specific aspect:
| Digit | Name | Value | Meaning |
|---|---|---|---|
| C | CPU-specific optimization | 1 | Enable AVX instruction generation |
| R | Register allocation | 2 | Use global register allocation |
| T | JIT trigger | 5 | Use tracing JIT (recommended) |
| O | Optimization level | 5 | Maximum optimization |
Common JIT configurations:
1255— Tracing JIT, full optimization (recommended for production)1205— Function JIT, full optimization (less memory, less aggressive)0ordisable— JIT disabled
PHP's JIT compiler is CPU-intensive during compilation. On a shared VPS, JIT compilation competes with other tenants for CPU time, which can cause inconsistent performance during the warm-up period. A Dedicated VPS ensures JIT compilation completes quickly because your CPU is not shared.
JIT Buffer Size
The jit_buffer_size controls how much memory is allocated for compiled machine code. Start with 64-128MB and monitor:
php -r "print_r(opcache_get_status()['jit']);"
If buffer_free is close to zero, increase the buffer size.
php.ini Tuning
Beyond FPM and OPcache, several php.ini settings affect performance and resource usage. Edit the FPM-specific php.ini:
sudo nano /etc/php/8.3/fpm/php.ini
Memory and Execution Limits
; Memory limit per worker process
; This directly affects max_children calculations
memory_limit = 256M
; Maximum execution time (seconds)
; Prevents runaway scripts from consuming a worker forever
max_execution_time = 30
; Maximum input time for POST data parsing
max_input_time = 60
; Maximum POST size (must be >= upload_max_filesize)
post_max_size = 64M
; Maximum file upload size
upload_max_filesize = 64M
; Maximum number of simultaneous file uploads
max_file_uploads = 20
Realpath Cache
PHP resolves file paths (following symlinks, checking existence) on every include/require. The realpath cache stores resolved paths in memory:
; Default is 4096 entries / 16K — far too low for frameworks
realpath_cache_size = 4096K
realpath_cache_ttl = 600
Modern frameworks like Laravel and Symfony include hundreds of files per request. Increasing the realpath cache eliminates thousands of filesystem calls per request.
Session Handling
If you use PHP sessions, configure them for performance:
; Use files by default (for single-server setups)
session.save_handler = files
session.save_path = "/var/lib/php/sessions"
; Session garbage collection
session.gc_maxlifetime = 1440
session.gc_probability = 1
session.gc_divisor = 1000
For multi-server setups or high traffic, switch sessions to Redis (see our Redis installation guide):
session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379"
Output Buffering
; Buffer output before sending to Nginx
; Reduces the number of write() system calls
output_buffering = 4096
; Implicit flush off (let the buffer do its job)
implicit_flush = Off
Need More PHP Concurrency?
If your max_children limit is regularly reached (check the FPM slow log and status page), you need more RAM. On a MassiveGRID Cloud VPS, you can add RAM independently without changing your CPU or storage allocation. Doubling your RAM from 4GB to 8GB roughly doubles your max_children capacity, letting you handle twice as many concurrent PHP requests.
Monitoring PHP-FPM
Enable the Status Page
The PHP-FPM status page shows real-time pool metrics. Enable it in the pool configuration:
sudo nano /etc/php/8.3/fpm/pool.d/www.conf
pm.status_path = /fpm-status
ping.path = /fpm-ping
ping.response = pong
Configure Nginx to serve it (restrict to localhost or trusted IPs):
location /fpm-status {
access_log off;
allow 127.0.0.1;
deny all;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
Reload both services:
sudo systemctl reload php8.3-fpm
sudo systemctl reload nginx
Query the status page:
curl -s http://127.0.0.1/fpm-status
Output:
pool: www
process manager: dynamic
start time: 28/Feb/2026:10:30:15 +0000
start since: 86400
accepted conn: 15234
listen queue: 0
max listen queue: 12
listen queue len: 128
idle processes: 14
active processes: 3
total processes: 17
max active processes: 42
max children reached: 0
Key metrics to watch:
listen queue— requests waiting for a worker. Should be 0 most of the time. If consistently > 0, increasemax_children.max children reached— number of times the pool hitmax_children. If > 0, you are running out of workers.active processesvsidle processes— helps you tunemin/max_spare_servers.
For full-format output with per-process details:
curl -s "http://127.0.0.1/fpm-status?full"
Enable the Slow Log
The PHP-FPM slow log captures stack traces of requests that exceed a time threshold — invaluable for finding bottlenecks:
; In /etc/php/8.3/fpm/pool.d/www.conf
slowlog = /var/log/php-fpm-slow.log
request_slowlog_timeout = 5s
request_slowlog_trace_depth = 20
This logs full stack traces for any request taking longer than 5 seconds. Check it regularly:
sudo tail -50 /var/log/php-fpm-slow.log
For monitoring dashboards and alerts that track PHP-FPM metrics over time, see our VPS monitoring setup guide.
Benchmarking: Before and After
Always benchmark before and after changes to verify improvements. Use ab (Apache Bench) or wrk for HTTP benchmarking.
Install wrk
sudo apt install wrk -y
Baseline Benchmark (Before Tuning)
# 4 threads, 100 concurrent connections, 30 seconds
wrk -t4 -c100 -d30s http://your-domain.com/
Record the results:
Running 30s test @ http://your-domain.com/
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 245ms 89ms 1.2s 78%
Req/Sec 52.3 18.7 120 65%
6234 requests in 30s, 45.2MB read
Requests/sec: 207.8
Transfer/sec: 1.5MB
Apply All Optimizations
After applying the PHP-FPM, OPcache, and JIT configurations above:
sudo systemctl restart php8.3-fpm
sudo systemctl restart nginx
Wait a few seconds for OPcache and JIT to warm up (hit the site a few times first), then benchmark again:
wrk -t4 -c100 -d30s http://your-domain.com/
Expected results after tuning:
Running 30s test @ http://your-domain.com/
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 48ms 22ms 320ms 82%
Req/Sec 538.7 95.2 780 71%
64380 requests in 30s, 467MB read
Requests/sec: 2146.0
Transfer/sec: 15.6MB
In this example, requests per second improved from 208 to 2,146 — a 10x improvement — and average latency dropped from 245ms to 48ms.
PHP-Specific Benchmarking
For isolating PHP performance from Nginx and network overhead, use the built-in PHP benchmarking:
# OPcache warm test
php -d opcache.enable_cli=1 -r "
\$start = microtime(true);
for (\$i = 0; \$i < 1000000; \$i++) {
\$x = sin(\$i) * cos(\$i);
}
\$end = microtime(true);
echo 'Time: ' . round((\$end - \$start) * 1000, 2) . ' ms' . PHP_EOL;
"
Run with and without JIT to see the difference on CPU-intensive code:
# Without JIT
php -d opcache.jit=0 -d opcache.enable_cli=1 -r "
\$start = microtime(true);
for (\$i = 0; \$i < 10000000; \$i++) { \$x = sin(\$i) * cos(\$i) * tan(\$i); }
echo round((microtime(true) - \$start) * 1000) . ' ms' . PHP_EOL;
"
# With JIT
php -d opcache.jit=1255 -d opcache.jit_buffer_size=64M -d opcache.enable_cli=1 -r "
\$start = microtime(true);
for (\$i = 0; \$i < 10000000; \$i++) { \$x = sin(\$i) * cos(\$i) * tan(\$i); }
echo round((microtime(true) - \$start) * 1000) . ' ms' . PHP_EOL;
"
On a typical VPS, JIT reduces this from approximately 2,800ms to 1,900ms — a 32% improvement on pure computation.
Complete Configuration Summary
For reference, here is the complete optimized configuration for a 4GB VPS running a PHP 8.3 application:
/etc/php/8.3/fpm/pool.d/www.conf
[www]
user = www-data
group = www-data
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 60
pm.start_servers = 15
pm.min_spare_servers = 10
pm.max_spare_servers = 25
pm.max_requests = 500
pm.process_idle_timeout = 10s
pm.status_path = /fpm-status
ping.path = /fpm-ping
slowlog = /var/log/php-fpm-slow.log
request_slowlog_timeout = 5s
request_slowlog_trace_depth = 20
php_admin_value[error_log] = /var/log/php-fpm-error.log
php_admin_flag[log_errors] = on
/etc/php/8.3/fpm/conf.d/10-opcache.ini
[opcache]
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.revalidate_freq=0
opcache.optimization_level=0x7FFEBFFF
opcache.save_comments=1
opcache.enable_file_override=1
opcache.jit=1255
opcache.jit_buffer_size=128M
Key php.ini Settings
memory_limit = 256M
max_execution_time = 30
max_input_time = 60
post_max_size = 64M
upload_max_filesize = 64M
realpath_cache_size = 4096K
realpath_cache_ttl = 600
output_buffering = 4096
Need Consistent Execution Speed?
On a shared VPS, CPU resources fluctuate based on other tenants' activity. This means your PHP-FPM workers might process a request in 20ms during quiet periods and 60ms during busy ones. If consistent response times matter — especially for e-commerce checkouts, API responses, or real-time applications — a Dedicated VPS guarantees your CPU is exclusively yours. No noisy neighbors, no variability.
Prefer Managed PHP Optimization?
Tuning PHP-FPM, OPcache, and JIT is a one-time effort that pays dividends for months. But keeping the configuration optimal as your traffic grows, monitoring for memory leaks, adjusting max_children after application updates, and troubleshooting slow log entries — that is ongoing work. MassiveGRID Managed Dedicated Servers include PHP performance optimization as part of the managed service. The operations team monitors your PHP-FPM metrics, adjusts pool sizes based on actual traffic patterns, and keeps OPcache and JIT tuned for your specific application.
Whether you manage PHP yourself or let a team handle it, the configuration principles in this guide apply. Start with the max_children formula, enable OPcache with validate_timestamps=0, benchmark before and after, and monitor the FPM status page. Those four steps alone will transform your PHP application's performance.