๐ Table of Contents
1. PHP-FPM Configuration & pm.max_children
Configuring PHP-FPM correctly is crucial because it significantly impacts the performance, stability, and security of your sites. Proper configuration ensures optimal resource allocation, which can handle the traffic loads efficiently and reduce latency. Misconfiguration can lead to issues like slow response times, excessive memory usage, and even server crashes, potentially resulting in downtime and poor user experience.
Understanding pm.max_children
The pm.max_children directive sets the limit on how many child processes PHP-FPM can create concurrently. This is the most important directive you need to configure. Setting it correctly ensures that PHP-FPM can manage the server's load efficiently, prevent resource exhaustion, and maintain optimal performance.
An incorrect value can lead to server instability or even crashes:
- Set too high: Exhausts server resources โ server crash
- Set too low: Insufficient request handling โ slow response times
Process Manager Modes
| Mode | Description | Best For |
|---|---|---|
| ondemand | Dynamically spawns PHP processes only when there is an incoming request. Conserves resources by creating processes on demand. | Most sites, especially with varying traffic levels and limited resources |
| static | Spawns a fixed number of PHP processes during startup and keeps them running continuously. | High-traffic sites with consistent demand and sufficient memory (32GB+ RAM) |
| dynamic | Combines aspects of both modes - starts with predefined processes and spawns additional ones as needed. | Not recommended - challenging to configure correctly |
Step-by-Step Configuration Process
Configuration Workflow
Step 1: Display Pool Usernames
First, identify all PHP-FPM pool users on your server:
grep -E '^\s*user\s*=' /etc/php/8.3/fpm/pool.d/*.conf | awk -F= '{print $2}' | xargs | tr ' ' '\n' | sort -u
Example Output: expert_wp, www-data
Step 2: Calculate Average Memory Per Pool User
Calculate the memory used by each pool user. Replace POOL_USER with the actual username:
ps -C php-fpm --user POOL_USER -o rss= | awk '{ sum += $1; count++ } END { if (count > 0) printf ("%d%s\n", sum/NR/1024,"M") }'
Example:
ps -C php-fpm --user expert_wp -o rss= | awk '{ sum += $1; count++ } END { if (count > 0) printf ("%d%s\n", sum/NR/1024,"M") }'
Output: 54M
Step 3: Check Free Memory with htop
htopReview the memory usage display. Look for the "Mem" line showing total and used memory.
Step 4: Calculate pm.max_children Value
Use the following formula:
Calculation Formula
pm.max_children = (Free Memory ร 0.9) รท Average Memory Per Process
Example Calculation:
- Total Memory: 956 MB
- Used Memory: 388 MB (round to 400 MB)
- Free Memory: 550 MB (conservative: 500 MB)
- 90% of Free Memory: 500 ร 0.9 = 450 MB
- Average Process Memory: 54 MB
- pm.max_children = 450 รท 54 = 8.33 โ Round down to 8
Step 5: Configure PHP-FPM Pool
Open your site's PHP pool configuration file:
cd /etc/php/8.3/fpm/pool.d/
sudo nano example.com.confFor ondemand mode (Recommended for most sites):
pm = ondemand
pm.max_children = 8
pm.process_idle_timeout = 10s
pm.max_requests = 500For static mode (High-traffic sites with 32GB+ RAM):
pm = static
pm.max_children = 8Step 6: Reload PHP-FPM
sudo systemctl reload php8.3-fpmMonitoring & Validation
Check PHP-FPM Logs for Warnings
sudo grep max_children /var/log/php8.3-fpm.logWARNING: [pool example.com] server reached max_children setting (8), consider raising it
If you see this warning:
- DO NOT simply increase pm.max_children without adding more RAM
- Your server doesn't have enough memory to handle more processes
- Consider upgrading server memory or implementing caching
- Serving cached pages bypasses PHP and reduces the need for more processes
Important Considerations
- No One-Size-Fits-All: Each site and server environment is unique. The optimal value depends on your specific memory usage patterns.
- Regular Monitoring: Check logs and resources weekly, especially after installing/removing themes, plugins, or WordPress updates.
- Caching Impact: Proper caching (FastCGI, Redis, etc.) significantly reduces PHP-FPM load by serving static cached pages.
- Minimum RAM: Servers should have at least 2GB of RAM. With only 1GB, hosting multiple sites is nearly impossible.
- Multiple Pools: When hosting multiple sites, recalculate and redistribute resources among all pools accordingly.
2. WordPress Configuration
Disable Post Revisions
Post revisions can bloat your database. Disable them to improve performance:
cd /var/www/example.com/
sudo nano public_html/wp-config.phpAdd this line to wp-config.php:
define('WP_POST_REVISIONS', false);Restart PHP-FPM:
sudo systemctl restart php8.3-fpmIncrease Memory Limit
Configure both PHP pool and WordPress memory limits:
PHP Pool Configuration
cd /etc/php/8.3/fpm/pool.d/
sudo nano example.com.confUpdate or add:
php_admin_value[memory_limit] = 256MWordPress Configuration
sudo nano /var/www/example.com/public_html/wp-config.phpAdd:
/** MEMORY LIMIT */
define('WP_MEMORY_LIMIT', '256M');Disable WordPress Cron
Replace WordPress's built-in cron with a proper system cron job for better reliability:
Disable WP-Cron in wp-config.php
define('DISABLE_WP_CRON', true);Restart PHP-FPM:
sudo systemctl restart php8.3-fpmSet Up System Cron Job
crontab -eAdd this line to run wp-cron every 15 minutes:
*/15 * * * * wget -q -O - https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1
3. OPcache Configuration
OPcache is a PHP bytecode cache that improves PHP performance by storing precompiled script bytecode in shared memory. This eliminates the need to load and parse PHP scripts on each request, significantly reducing CPU usage and improving response times.
Configure OPcache Settings
cd /etc/php/8.3/fpm/pool.d/
sudo nano example.com.confDevelopment Server Configuration
; OPCACHE CONFIGURATION - DEVELOPMENT SERVER
; opcache.enabled is on by default - leave commented
; php_admin_flag[opcache.enabled] = 1
php_admin_value[opcache.memory_consumption] = 256
php_admin_value[opcache.interned_strings_buffer] = 32
php_admin_value[opcache.max_accelerated_files] = 20000
php_admin_flag[opcache.validate_timestamps] = 1
php_admin_value[opcache.revalidate_freq] = 2
php_admin_flag[opcache.validate_permission] = 1Production Server Configuration
; OPCACHE CONFIGURATION - PRODUCTION SERVER
; opcache.enabled is on by default - leave commented
;php_admin_flag[opcache.enabled] = 1
;php_admin_value[opcache.memory_consumption] = 256
;php_admin_value[opcache.interned_strings_buffer] = 32
;php_admin_value[opcache.max_accelerated_files] = 20000
;php_admin_value[opcache.validate_timestamps] = 0
;php_admin_flag[opcache.validate_permission] = 1- validate_timestamps: Set to 1 for development (checks for file changes), 0 for production (better performance)
- revalidate_freq: Only needed in development mode - how often to check for file changes
Calculate max_accelerated_files
Count the number of PHP files in your WordPress installation:
cd /var/www/
sudo find . -type f -print | grep php | wc -lSet max_accelerated_files to a value higher than the count (e.g., if you have 15,000
files, set it to 20,000).
Apply Configuration
sudo systemctl reload php8.3-fpmReference: PHP OPcache Documentation
4. FastCGI Caching with NGINX
FastCGI caching allows NGINX to cache PHP-generated content directly. When a page is cached, NGINX serves it from memory/disk without invoking PHP-FPM, dramatically reducing server load and improving response times.
Step 1: Configure HTTP Context
cd /etc/nginx
sudo nano nginx.confAdd this before the # Virtual Host Configs section:
### FASTCGI CACHING
# fastcgi_cache_path directive - PATH & NAME must be unique for each site
fastcgi_cache_path /var/run/example levels=1:2 keys_zone=EXAMPLE:100m inactive=60m;
# applied to all sites
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header http_500;
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;fastcgi_cache_path with
unique PATH and NAME (keys_zone).
Step 2: Create Cache Exclusion Rules
cd /etc/nginx/includes/
sudo nano fastcgi_cache_excludes.conf# NGINX SKIP CACHE INCLUDE FILE
set $skip_cache 0;
# POST requests and urls with a query string should always go to PHP
if ($request_method = POST) {
set $skip_cache 1;
}
if ($query_string != "") {
set $skip_cache 1;
}
# Don't cache uris containing the following segments
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
set $skip_cache 1;
}
# Don't use the cache for logged in users or recent commenters
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
set $skip_cache 1;
}Step 3: Configure Server Block
cd /etc/nginx/sites-available
sudo nano example.com.confAdd these directives to your server block:
include /etc/nginx/includes/fastcgi_cache_excludes.conf;
add_header X-FastCGI-Cache $upstream_cache_status;In the PHP location block, add:
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache EXAMPLE;
fastcgi_cache_valid 60m;Step 4: Add Cache Purge Location (Optional)
location ~ /purge(/.*) {
fastcgi_cache_purge EXAMPLE "$scheme$request_method$host$1";
}Step 5: Test and Reload
sudo nginx -t
sudo systemctl reload nginxVerify Caching
curl -I https://example.comLook for the X-FastCGI-Cache header:
- MISS - Page not in cache (first request)
- HIT - Page served from cache
- BYPASS - Caching bypassed based on rules
Removing FastCGI Caching
To disable FastCGI caching:
- Remove the
include /etc/nginx/includes/fastcgi_cache_excludes.conf;line - Comment out all fastcgi_cache directives in the server block
- Remove the cache purge location block
- Remove the X-FastCGI-Cache header
- Test and reload NGINX
- Restart PHP-FPM to clear OPcache
5. WP Super Cache Configuration
WP Super Cache is a WordPress plugin that generates static HTML files from your dynamic WordPress site. NGINX can serve these static files directly without processing PHP, resulting in extremely fast page loads.
Configure NGINX for WP Super Cache
Step 1: Create Exclusion Rules
cd /etc/nginx/includes/
sudo nano wp_super_cache_excludes.conf# WP Super Cache NGINX Cache Exclusions Rules
set $cache_uri $request_uri;
# POST requests and urls with a query string should always go to PHP
if ($request_method = POST) {
set $cache_uri 'null cache';
}
if ($query_string != "") {
set $cache_uri 'null cache';
}
# Don't cache uris containing the following segments
if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|/wp-(app|cron|login|register|mail).php|wp-.*.php|/feed/|index.php|wp-comments-popup.php|wp-links-opml.php|wp-locations.php|sitemap(_index)?.xml|[a-z0-9_-]+-sitemap([0-9]+)?.xml)") {
set $cache_uri 'null cache';
}
# Don't use the cache for logged in users or recent commenters
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in") {
set $cache_uri 'null cache';
}
# Use cached or actual file if they exists, otherwise pass request to WordPress
location / {
try_files /wp-content/cache/supercache/$http_host/$cache_uri/index-https.html $uri $uri/ /index.php?$args ;
}Step 2: Update Server Block
cd /etc/nginx/sites-available/
sudo nano example.com.confAdd the include directive:
include /etc/nginx/includes/wp_super_cache_excludes.conf;Step 3: Test and Reload
sudo nginx -t
sudo systemctl reload nginxUninstalling WP Super Cache
- Deactivate and delete the plugin from WordPress
- Remove the include directive from your NGINX config
- Uncomment the standard location block
- Test and reload NGINX
- Reload PHP-FPM
cd /etc/nginx/sites-available/
sudo nano example.com.confRemove:
include /etc/nginx/includes/wp_super_cache_exclusions.conf;
Uncomment:
location / {
try_files $uri $uri/ /index.php$is_args$args;
}sudo nginx -t
sudo systemctl reload nginx
sudo systemctl reload php8.3-fpm6. W3 Total Cache Configuration
W3 Total Cache (W3TC) is a comprehensive WordPress caching plugin that integrates with NGINX. It provides page caching, object caching, database caching, and minification features.
Preparation
Step 1: Create nginx.conf File
cd /var/www/example.com/public_html/
sudo touch nginx.conf
sudo chown username:username nginx.conf
sudo chmod 660 nginx.conf
sudo ls -lStep 2: Secure nginx.conf
cd /etc/nginx/includes/
sudo nano nginx_security_directives.confAdd:
location = /nginx.conf { deny all; }Configure NGINX for W3TC
Step 1: Create Exclusion Rules
cd /etc/nginx/includes/
sudo nano w3tc_cache_excludes.conf# W3 TOTAL CACHE EXCLUDES FILE
set $cache_uri $request_uri;
# POST requests and urls with a query string should always go to PHP
if ($request_method = POST) {
set $cache_uri 'null cache';
}
if ($query_string != "") {
set $cache_uri 'null cache';
}
# Don't cache uris containing the following segments
if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|/wp-(app|cron|login|register|mail).php|wp-.*.php|/feed/|index.php|wp-comments-popup.php|wp-links-opml.php|wp-locations.php|sitemap(_index)?.xml|[a-z0-9_-]+-sitemap([0-9]+)?.xml)") {
set $cache_uri 'null cache';
}
# Don't use the cache for logged in users or recent commenters
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in") {
set $cache_uri 'null cache';
}
# Use cached or actual file if file exists, otherwise pass request to WordPress
location / {
try_files /wp-content/w3tc/pgcache/$cache_uri/_index.html $uri $uri/ /index.php?$args;
}Step 2: Update Server Block
cd /etc/nginx/sites-available/
sudo nano example.com.confComment out the default location block and add:
#location / {
# try_files $uri $uri/ /index.php$is_args$args;
#}
include /etc/nginx/includes/w3tc_cache_excludes.conf;
include /var/www/example.com/public_html/nginx.conf;Step 3: Install PHP Tidy Extension
sudo apt update
sudo apt install php8.3-tidyStep 4: Reload Services
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl reload php8.3-fpmUninstalling W3 Total Cache
- Deactivate and delete the plugin from WordPress
- Restore the default location block
- Remove the include directives
- Delete cache directories and nginx.conf file
- Reload services
cd /etc/nginx/sites-available/
sudo nano example.com.confRestore:
location / {
try_files $uri $uri/ /index.php$is_args$args;
}Remove includes and delete cache files:
cd /var/www/example.com/public_html/wp-content/
sudo rm -rf cache/ w3tc-config/
cd /var/www/example.com/public_html/
sudo rm nginx.conf
sudo systemctl reload php8.3-fpm
sudo systemctl reload nginx7. Redis Object Caching
Redis is an in-memory data structure store that can be used as a database cache. For WordPress, it provides object caching, which stores database query results in memory for faster retrieval, dramatically reducing database load.
Installation
sudo apt update
sudo apt install redis-server php8.3-redisVerify Installation
sudo systemctl status redis-server
sudo cat /var/log/redis/redis-server.logFix Common Warning
If you see: WARNING overcommit_memory is set to 0!
cd /etc/sysctl.d/
sudo nano 11-redis.confAdd:
vm.overcommit_memory = 1sudo rebootConfigure Redis
cd /etc/
sudo nano redis/redis.confSet memory limit and eviction policy:
maxmemory 256mb
maxmemory-policy allkeys-lrusudo systemctl restart redis-server
sudo rebootConfigure WordPress for Redis
cd /var/www/example.com/public_html/
sudo nano wp-config.phpAdd cache key salt:
define( 'WP_CACHE_KEY_SALT', 'example.com' );WooCommerce Compatibility
If using WooCommerce, prevent Redis from caching session data:
/** PREVENT REDIS CACHING WOO SESSION DATA */
define('WP_REDIS_IGNORED_GROUPS', 'wc_session');Ensure the following pages/cookies are excluded from caching:
- Pages: /cart/, /my-account/, /checkout/
- Cookies: woocommerce_cart_hash, woocommerce_items_in_cart, wp_woocommerce_session_*, woocommerce_recently_viewed
Reference: WooCommerce Caching Configuration
8. Cloudflare Integration
When using Cloudflare as a reverse proxy, NGINX sees Cloudflare's IP addresses instead of the actual visitor IPs. Configuring NGINX to read the real client IP from Cloudflare headers is essential for accurate logging, security, and analytics.
Important: Auto Minify Deprecation
Cloudflare has deprecated the Auto Minify feature. For more information, see: Cloudflare Community Discussion
Configure NGINX for Cloudflare
Step 1: Get Current Cloudflare IP Ranges
Visit these URLs to get the latest Cloudflare IP ranges:
Step 2: Create Cloudflare IP List
cd /etc/nginx/includes
sudo nano cloudflare_ip_list.confAdd all Cloudflare IP ranges (example from document):
# Last updated OCT 2022 - Update regularly from cloudflare.com/ips
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;
real_ip_header CF-Connecting-IP;Step 3: Include in Server Block
cd /etc/nginx/sites-available/
sudo nano example.com.confAdd the include directive:
include /etc/nginx/includes/cloudflare_ip_list.conf;Step 4: Test and Reload
sudo nginx -t
sudo systemctl reload nginxVerification
Check your NGINX access logs to verify that real visitor IPs are now being logged instead of Cloudflare IPs:
sudo tail -f /var/log/nginx/example.com-access.logSummary & Best Practices
Key Takeaways
- PHP-FPM Configuration: Always calculate pm.max_children based on available memory and average process size
- Process Manager Mode: Use 'ondemand' for most sites, 'static' only for high-traffic sites with 32GB+ RAM
- Regular Monitoring: Check logs weekly and after major changes (plugins, themes, WordPress updates)
- Caching Strategy: Implement appropriate caching (FastCGI, Redis, plugin-based) to reduce PHP-FPM load
- Memory Requirements: Minimum 2GB RAM recommended; more for multiple sites
- OPcache: Essential for performance - configure differently for dev vs production
- WordPress Optimization: Disable post revisions, use system cron, set appropriate memory limits
- Cloudflare Integration: Update IP ranges regularly for accurate visitor tracking
Performance Optimization Stack
Layer 1: Edge Caching โ Cloudflare CDN
Layer 2: NGINX Caching โ FastCGI Cache / WP Super Cache / W3TC
Layer 3: Object Caching โ Redis
Layer 4: Opcode Caching โ OPcache
Layer 5: PHP-FPM โ Properly configured pm.max_children
Layer 6: Database โ MySQL/MariaDB optimization
Common Pitfalls to Avoid
- Setting pm.max_children too high without sufficient RAM
- Using 'static' mode on low-memory servers
- Forgetting to monitor logs after configuration changes
- Not updating Cloudflare IP ranges regularly
- Caching WooCommerce cart/checkout pages
- Not recalculating resources when adding new sites
- Ignoring the php-fpm.log warnings
Monitoring Commands Reference
# Monitor system resources
htop
# Check PHP-FPM pool users
grep -E '^\s*user\s*=' /etc/php/8.3/fpm/pool.d/*.conf | awk -F= '{print $2}' | xargs | tr ' ' '\n' | sort -u
# Calculate memory per pool user
ps -C php-fpm --user POOL_USER -o rss= | awk '{ sum += $1; count++ } END { if (count > 0) printf ("%d%s\n", sum/NR/1024,"M") }'
# Check for max_children warnings
sudo grep max_children /var/log/php8.3-fpm.log
# Monitor NGINX access logs
sudo tail -f /var/log/nginx/example.com-access.log
# Check FastCGI cache status
curl -I https://example.com
# Monitor Redis
redis-cli INFO