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:

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:

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:

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

When JIT Does Not Help Much

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:

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:

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.