Why PHP Performance Tuning Matters
A freshly installed PHP server with default settings is like a sports car in first gear. It works, but it's not using anywhere near its full potential. PHP's default configuration is designed for broad compatibility and safety, not for performance. With the right tuning, you can typically achieve a 3x to 10x improvement in throughput without changing a single line of application code.
PHP performance optimization operates on three levels: OPcache (how PHP compiles and caches code), php.ini settings (how PHP executes code), and PHP-FPM pool configuration (how PHP handles concurrent requests). This guide covers all three, with production-tested settings and the reasoning behind each optimization.
Compile once
Execute efficiently
Handle concurrency
Native compilation
OPcache: The Single Biggest Performance Win
OPcache is PHP's built-in opcode cache. Without OPcache, PHP parses and compiles every PHP file on every request. With OPcache enabled, PHP compiles each file once and stores the compiled bytecode in shared memory. Subsequent requests skip the entire parsing and compilation step, using the cached bytecode directly.
Optimal OPcache Configuration
; Add to php.ini or a dedicated opcache.ini
opcache.enable=1
; Enable OPcache (should be 1 in production)
opcache.memory_consumption=256
; Shared memory size in MB for storing compiled scripts
; 128MB for small sites, 256MB for medium, 512MB for large
opcache.interned_strings_buffer=32
; Memory for interned strings (comments, variable names, etc.)
; 16MB minimum, 32MB recommended for frameworks
opcache.max_accelerated_files=20000
; Max number of PHP files to cache
; WordPress: ~5000, Laravel: ~15000, Magento: ~50000
; Use: find . -name "*.php" | wc -l to count
opcache.revalidate_freq=60
; How often (seconds) to check for file changes
; 0 = check every request (dev), 60+ for production
opcache.validate_timestamps=1
; Check if files have changed (1=yes, 0=never check)
; Set to 0 on deploy-based workflows for max performance
opcache.save_comments=1
; Keep docblock comments (required by many frameworks)
opcache.enable_file_override=1
; Optimize file_exists/is_file/is_readable checks
Understanding OPcache Settings
| Setting | Development | Production (Shared) | Production (Dedicated) |
|---|---|---|---|
| opcache.enable | 1 | 1 | 1 |
| memory_consumption | 128 | 256 | 512 |
| interned_strings_buffer | 16 | 32 | 64 |
| max_accelerated_files | 10000 | 20000 | 50000 |
| revalidate_freq | 0 | 60 | 120 |
| validate_timestamps | 1 | 1 | 0 |
opcache_reset() in a deployment script, or restart PHP-FPM after each deployment.
Monitoring OPcache Status
$ php -r "print_r(opcache_get_status(false));"
Array (
[opcache_enabled] => 1
[memory_usage] => Array (
[used_memory] => 67108864
[free_memory] => 201326592
[wasted_memory] => 0
[current_wasted_percentage] => 0.00
)
[opcache_statistics] => Array (
[num_cached_scripts] => 4521
[opcache_hit_rate] => 99.87
[misses] => 4521
[hits] => 3456789
)
)
Key metrics to watch: hit rate should be above 99%. If it's lower, your max_accelerated_files might be too small or memory_consumption too low. wasted_percentage above 5% indicates too-frequent cache invalidation.
JIT Compilation: Machine-Level Optimization
PHP 8.0 introduced the Just-In-Time (JIT) compiler, which takes OPcache one step further by compiling PHP bytecode into native machine code. While OPcache eliminates parsing and compilation overhead, JIT eliminates the bytecode interpretation overhead entirely for hot code paths.
opcache.jit=1255
; JIT mode: 1=enable, 2=tracing (best for web), 5=optimize, 5=all
; 1255 = tracing JIT with full optimization
opcache.jit_buffer_size=128M
; Memory for compiled machine code
; 64M for small sites, 128M-256M for larger ones
When JIT Helps
- CPU-intensive operations (image processing, PDF generation)
- Mathematical computations and data transformations
- Tight loops with repetitive logic
- Applications with complex business logic
When JIT Has Less Impact
- I/O-bound applications (most web apps)
- Applications that spend most time waiting on database queries
- Simple CRUD applications with minimal computation
- Applications with very little PHP code per request
php.ini Optimization
Beyond OPcache, several php.ini settings have a significant impact on performance. Here are the most important ones:
Memory and Execution Limits
memory_limit = 256M
; WordPress: 256M, Laravel: 256M, Magento: 768M
; Don't set higher than needed - wastes RAM on multi-user servers
; Execution time limits
max_execution_time = 30
; Max seconds a script can run (web requests)
; CLI scripts ignore this setting
max_input_time = 60
; Max seconds for parsing input data (POST, file uploads)
; Input handling
max_input_vars = 3000
; Max number of input variables per request
; Some CMS/shop systems need 5000+
Realpath Cache
The realpath cache stores resolved filesystem paths, preventing PHP from repeatedly performing disk I/O to resolve file paths. This is one of the most underappreciated PHP performance settings.
realpath_cache_size = 4096k
; Default 4k is far too small. 4096k handles most frameworks.
realpath_cache_ttl = 600
; How long to cache paths (seconds). 600 = 10 minutes.
; Default 120 is fine for dev, increase for production.
Session and Output Handling
output_buffering = 4096
; Buffer output in 4KB chunks instead of flushing byte-by-byte
; Reduces number of write() syscalls significantly
; Compression
zlib.output_compression = Off
; Let Nginx/Apache handle gzip compression instead
; Web server compression is more efficient
; Error handling (production)
display_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
; Don't display errors to users, log them instead
PHP-FPM Pool Configuration
PHP-FPM (FastCGI Process Manager) is how PHP handles concurrent requests. Each PHP-FPM worker process handles one request at a time, so the number of workers directly determines how many simultaneous PHP requests your server can process.
Process Manager Types
| PM Type | Behavior | Best For | Trade-off |
|---|---|---|---|
| static | Fixed number of workers always running | Dedicated servers with consistent traffic | Uses more RAM at idle, fastest response |
| dynamic | Workers scale between min and max | Shared hosting, variable traffic | Balance between RAM usage and performance |
| ondemand | Workers spawn only when needed | Low-traffic sites, many pools | Slower first request, lowest RAM |
Calculating pm.max_children
The most critical PHP-FPM setting is pm.max_children, which determines how many concurrent PHP requests your server can handle. Setting it too low causes request queuing; too high causes memory exhaustion.
pm.max_children = (Total RAM - RAM for OS and other services) / Average PHP process sizeFor example, on a server with 4GB RAM, 1GB used by OS/MySQL/Nginx:
(4096MB - 1024MB) / 40MB per process = 76 max_childrenCheck average process size with:
ps -eo rss,comm | grep php-fpm | awk '{sum+=$1; n++} END {print sum/n/1024 " MB"}'
Optimal Dynamic Pool Configuration
; /etc/php/8.4/fpm/pool.d/www.conf
pm = dynamic
; Use dynamic process management
pm.max_children = 50
; Maximum number of child processes
; Based on your RAM calculation above
pm.start_servers = 10
; Workers created on startup
; Formula: min_spare + (max_spare - min_spare) / 2
pm.min_spare_servers = 5
; Minimum idle workers kept alive
pm.max_spare_servers = 15
; Maximum idle workers before culling
pm.max_requests = 500
; Recycle worker after N requests (prevents memory leaks)
; 0 = never recycle (not recommended)
pm.process_idle_timeout = 10s
; Kill idle workers after this time (ondemand only)
Static Pool for Dedicated Servers
On a dedicated server handling only PHP workloads, the static process manager eliminates the overhead of spawning and killing worker processes:
pm = static
pm.max_children = 50
pm.max_requests = 1000
; All 50 workers start immediately and stay running
; No spawn delay on traffic spikes
; RAM usage is constant and predictable
PHP-FPM Slow Log
The slow log is an invaluable debugging tool that captures a stack trace whenever a PHP request takes longer than a specified threshold. This helps identify performance bottlenecks in your application code.
slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 5s
; Log a stack trace for any request taking longer than 5 seconds
; Example slow log output:
[17-Mar-2026 10:23:45] [pool www] pid 12345
script_filename = /var/www/html/index.php
[0x00007f1234] +5.123 sleep() /var/www/html/wp-content/plugins/slow-plugin/bad-code.php:42
[0x00007f1238] process_data() /var/www/html/wp-content/plugins/slow-plugin/main.php:156
PHP-FPM Status Page
Enable the PHP-FPM status page to monitor pool health in real-time:
pm.status_path = /fpm-status
; Output (with ?full parameter)
pool: www
process manager: dynamic
start time: 17/Mar/2026:10:00:00
accepted conn: 458291
listen queue: 0 ← Should be 0! Non-zero = need more workers
idle processes: 8
active processes: 2
total processes: 10
max active processes: 42 ← Peak concurrent, should be < max_children
max children reached: 0 ← Should be 0! Non-zero = max_children too low
listen queue is consistently above 0, requests are waiting for available PHP workers. Increase pm.max_children. If max children reached is greater than 0, your pool has hit its worker limit. This means some requests were delayed waiting for a free worker.
Benchmarking Your Configuration
After making changes, benchmark to measure the impact. Use ab (Apache Bench) or wrk for HTTP benchmarking:
$ ab -n 1000 -c 10 https://example.com/
Requests per second: 185.42 [#/sec] (mean)
Time per request: 53.930 [ms] (mean)
Transfer rate: 2456.78 [Kbytes/sec] received
# wrk: 10 threads, 100 connections, 30 seconds
$ wrk -t10 -c100 -d30s https://example.com/
Running 30s test @ https://example.com/
10 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 54.23ms 12.45ms 203ms 78.9%
Req/Sec 186.54 22.31 312 71.2%
55892 requests in 30.01s, 724.56MB read
Requests/sec: 1862.89
Complete Production php.ini Template
Here's a complete set of performance-oriented php.ini settings for production use:
opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 20000
opcache.revalidate_freq = 60
opcache.validate_timestamps = 1
opcache.save_comments = 1
opcache.enable_file_override = 1
opcache.jit = 1255
opcache.jit_buffer_size = 128M
; === MEMORY & EXECUTION ===
memory_limit = 256M
max_execution_time = 30
max_input_time = 60
max_input_vars = 3000
; === PATH CACHE ===
realpath_cache_size = 4096k
realpath_cache_ttl = 600
; === OUTPUT ===
output_buffering = 4096
zlib.output_compression = Off
; === ERROR HANDLING ===
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
; === FILE UPLOADS ===
upload_max_filesize = 64M
post_max_size = 64M
max_file_uploads = 20
PHP Performance on Panelica
Panelica provides per-user PHP-FPM pools with individual resource limits enforced through Cgroups v2. This architecture ensures that one user's PHP processes can never consume another user's resources, even under heavy load.
- Per-user, per-PHP-version FPM pools with isolated resource limits (CPU, memory)
- OPcache management per pool from the panel GUI
- php.ini settings configurable per user and per PHP version
- PHP-FPM pool settings (pm.max_children, pm type) adjustable per user
- Cgroups v2 enforcement prevents any single user from monopolizing server resources
- Switch between PHP 8.1, 8.2, 8.3, and 8.4 per domain with one click
On a shared hosting server, Panelica's per-user PHP-FPM pools provide a critical isolation layer. Each user gets their own pool with their own pm.max_children, their own OPcache, and their own resource limits. A traffic spike on one site cannot starve PHP workers from other sites.
Performance Tuning Checklist
Use this checklist to ensure you've covered all the major PHP performance optimizations:
- OPcache enabled with sufficient memory and max_accelerated_files
- OPcache hit rate above 99% (check with opcache_get_status)
- JIT enabled if PHP 8.0+ and CPU-intensive workloads
- realpath_cache_size increased from default 4k to 4096k
- PHP-FPM pm.max_children calculated based on available RAM
- PHP-FPM listen queue consistently at 0 (check status page)
- Slow log enabled to catch problematic scripts
- pm.max_requests set to prevent worker memory leaks
- output_buffering enabled (4096)
- display_errors off in production, log_errors on
- Benchmark before and after changes to measure impact
Conclusion
PHP performance tuning is not a one-time task but a process of measuring, adjusting, and verifying. The three pillars we covered, OPcache configuration, php.ini optimization, and PHP-FPM pool tuning, together can transform a sluggish PHP application into a responsive, high-throughput system.
Start with OPcache, as it provides the biggest return for the least effort. Then optimize your php.ini settings based on your application's specific needs. Finally, tune your PHP-FPM pool configuration to match your server's resources and traffic patterns. Benchmark after each change, and you'll have a PHP environment that performs at its best, whether you're handling 10 requests per second or 1,000.