๐Ÿš€ PHP-FPM & NGINX Configuration Guide

Professional WordPress Server Optimization

1. PHP-FPM Configuration & pm.max_children

Why is PHP-FPM Configuration Critical?

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.

โš ๏ธ Critical Warning:

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

1. Display Pool Users
โ†“
2. Calculate Memory Per Pool
โ†“
3. Check Free Memory (htop)
โ†“
4. Calculate pm.max_children
โ†“
5. Update Pool Configuration
โ†“
6. Monitor & Adjust

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

Note: The www-data user (default pool) typically uses minimal memory and can be ignored for calculations as it's not actively serving sites.

Step 3: Check Free Memory with htop

htop

Review the memory usage display. Look for the "Mem" line showing total and used memory.

Important: Be conservative when calculating free memory. Your server is likely idling during configuration. As traffic increases, resource requirements will also increase.

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
โœ“ Result: Set pm.max_children = 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.conf
For ondemand mode (Recommended for most sites):
pm = ondemand pm.max_children = 8 pm.process_idle_timeout = 10s pm.max_requests = 500
For static mode (High-traffic sites with 32GB+ RAM):
pm = static pm.max_children = 8

Step 6: Reload PHP-FPM

sudo systemctl reload php8.3-fpm

Monitoring & Validation

Check PHP-FPM Logs for Warnings

sudo grep max_children /var/log/php8.3-fpm.log
Warning to Watch For:

WARNING: [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.php

Add this line to wp-config.php:

define('WP_POST_REVISIONS', false);

Restart PHP-FPM:

sudo systemctl restart php8.3-fpm

Increase 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.conf

Update or add:

php_admin_value[memory_limit] = 256M

WordPress Configuration

sudo nano /var/www/example.com/public_html/wp-config.php

Add:

/** 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-fpm

Set Up System Cron Job

crontab -e

Add 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

What is OPcache?

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.conf

Development 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] = 1

Production 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
Key Differences:
  • 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 -l

Set 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-fpm

Reference: PHP OPcache Documentation

4. FastCGI Caching with NGINX

What is FastCGI Caching?

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.conf

Add 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;
Important: For each site, create a unique 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.conf

Add 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 nginx

Verify Caching

curl -I https://example.com

Look 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:

  1. Remove the include /etc/nginx/includes/fastcgi_cache_excludes.conf; line
  2. Comment out all fastcgi_cache directives in the server block
  3. Remove the cache purge location block
  4. Remove the X-FastCGI-Cache header
  5. Test and reload NGINX
  6. Restart PHP-FPM to clear OPcache

5. WP Super Cache Configuration

About WP Super Cache

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.conf

Add the include directive:

include /etc/nginx/includes/wp_super_cache_excludes.conf;

Step 3: Test and Reload

sudo nginx -t sudo systemctl reload nginx

Uninstalling WP Super Cache

  1. Deactivate and delete the plugin from WordPress
  2. Remove the include directive from your NGINX config
  3. Uncomment the standard location block
  4. Test and reload NGINX
  5. Reload PHP-FPM
cd /etc/nginx/sites-available/ sudo nano example.com.conf

Remove:

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-fpm

6. W3 Total Cache Configuration

About W3 Total Cache

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 -l

Step 2: Secure nginx.conf

cd /etc/nginx/includes/ sudo nano nginx_security_directives.conf

Add:

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.conf

Comment 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-tidy

Step 4: Reload Services

sudo nginx -t sudo systemctl reload nginx sudo systemctl reload php8.3-fpm

Uninstalling W3 Total Cache

  1. Deactivate and delete the plugin from WordPress
  2. Restore the default location block
  3. Remove the include directives
  4. Delete cache directories and nginx.conf file
  5. Reload services
cd /etc/nginx/sites-available/ sudo nano example.com.conf

Restore:

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 nginx

7. Redis Object Caching

What is Redis?

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-redis

Verify Installation

sudo systemctl status redis-server sudo cat /var/log/redis/redis-server.log

Fix Common Warning

If you see: WARNING overcommit_memory is set to 0!

cd /etc/sysctl.d/ sudo nano 11-redis.conf

Add:

vm.overcommit_memory = 1
sudo reboot

Configure Redis

cd /etc/ sudo nano redis/redis.conf

Set memory limit and eviction policy:

maxmemory 256mb maxmemory-policy allkeys-lru
sudo systemctl restart redis-server sudo reboot

Configure WordPress for Redis

cd /var/www/example.com/public_html/ sudo nano wp-config.php

Add 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');
WooCommerce Cache Exclusions:

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

Why 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.conf

Add 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;
โš ๏ธ Important: Update this list regularly as Cloudflare may add new IP ranges. Check the Cloudflare IP pages periodically.

Step 3: Include in Server Block

cd /etc/nginx/sites-available/ sudo nano example.com.conf

Add the include directive:

include /etc/nginx/includes/cloudflare_ip_list.conf;

Step 4: Test and Reload

sudo nginx -t sudo systemctl reload nginx

Verification

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.log

Summary & 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