Tutorial

PHP Performance Tuning: OPcache, php.ini, and PHP-FPM Optimization

May 07, 2026

Back to Blog

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.

OPcache
Compile once
php.ini
Execute efficiently
PHP-FPM
Handle concurrency
JIT
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.

Impact of enabling OPcache: On a typical WordPress site, enabling OPcache with optimal settings improves response time by 3-5x and increases throughput by 200-400%. It's the single most impactful PHP performance optimization you can make, and it requires zero application code changes.

Optimal OPcache Configuration

; OPcache configuration for production
; 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

SettingDevelopmentProduction (Shared)Production (Dedicated)
opcache.enable111
memory_consumption128256512
interned_strings_buffer163264
max_accelerated_files100002000050000
revalidate_freq060120
validate_timestamps110
validate_timestamps=0 warning: When set to 0, OPcache never checks if a PHP file has been modified. This gives maximum performance but means you must manually clear the OPcache after deploying new code. Use opcache_reset() in a deployment script, or restart PHP-FPM after each deployment.

Monitoring OPcache Status

# Quick OPcache status check from CLI
$ 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.

; JIT configuration (requires OPcache enabled)

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
Practical advice: For typical web applications (WordPress, Laravel CRUD, etc.), JIT provides a modest 5-15% improvement because the bottleneck is usually database queries and I/O, not PHP execution. For CPU-intensive workloads, JIT can provide 2-3x improvements. Enable it if your server has the memory to spare, but don't expect miracles for standard web apps.

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 per script
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 - reduces filesystem stat() calls
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
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 TypeBehaviorBest ForTrade-off
staticFixed number of workers always runningDedicated servers with consistent trafficUses more RAM at idle, fastest response
dynamicWorkers scale between min and maxShared hosting, variable trafficBalance between RAM usage and performance
ondemandWorkers spawn only when neededLow-traffic sites, many poolsSlower 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.

The formula:
pm.max_children = (Total RAM - RAM for OS and other services) / Average PHP process size

For example, on a server with 4GB RAM, 1GB used by OS/MySQL/Nginx:
(4096MB - 1024MB) / 40MB per process = 76 max_children

Check 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

; PHP-FPM 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:

; Static pool - all workers always running
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.

; Enable slow log in pool configuration
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:

; In pool configuration
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
Critical metrics to watch: If 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:

# Apache Bench: 1000 requests, 10 concurrent
$ 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
Benchmarking tips: Always benchmark from a different machine than the server to avoid skewing results. Run benchmarks multiple times and average the results. Test with realistic concurrency levels that match your actual traffic patterns, not extreme loads that overwhelm the server.

Complete Production php.ini Template

Here's a complete set of performance-oriented php.ini settings for production use:

; === OPCACHE ===
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.

Panelica PHP performance features:
  • 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.

Share:
See the Demo