WordPress Performance Optimization Guide

Complete Guide to Caching Strategies for NGINX & WordPress

Table of Contents

1. Caching Overview

This guide covers the four primary types of caching for WordPress optimization:

WordPress Caching Architecture

Browser Cache (Static Assets)
Page Cache (Pre-built HTML)
Object Cache (Database Queries)
OPcode Cache (Compiled PHP)
Database (MariaDB/MySQL)
Cache Type Purpose Impact Implementation
Page Cache Stores pre-built HTML pages Highest - Bypasses PHP & Database FastCGI, WP Super Cache, W3TC
Object Cache Caches database query results High - Reduces DB load Redis, Memcached
OPcode Cache Stores compiled PHP bytecode Medium - Reduces compilation OPcache (built-in PHP)
Browser Cache Stores static assets locally Medium - Reduces bandwidth NGINX headers
Performance Priority: Always enable page caching when possible. It provides the most significant performance improvement by serving pre-built HTML pages, completely bypassing PHP processing and database queries.

2. Page Caching (FastCGI Cache)

2.1 How Page Caching Works

Page Cache Request Flow

Client Request
NGINX: Check Cache
Cache Hit? → Serve HTML
Cache Miss? → PHP + DB
Store in Cache & Serve

When a client requests a page, the web server first checks for a pre-built HTML copy in the FastCGI cache. If found, it's immediately served to the client. If not found, the request is forwarded to PHP for processing, which generates the HTML dynamically.

2.2 FastCGI Cache Configuration

Step 1: Configure NGINX HTTP Context

Navigate to NGINX configuration:
cd /etc/nginx
sudo nano nginx.conf

Add the following directives above the "Virtual Host Configs" comment in the HTTP context:

FastCGI Cache Directives (add to nginx.conf):
### FASTCGI CACHING
# fastcgi_cache_path directive - PATH & NAME must be unique for each site
fastcgi_cache_path /var/run/SITE levels=1:2 keys_zone=NAME:100m inactive=60m;
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: Replace SITE with your site identifier and NAME with a unique cache zone name for each site.

Step 2: Create Cache Exclusion Rules

Create exclusion file:
cd /etc/nginx/includes/
sudo nano fastcgi_cache_excludes.conf
Cache exclusion rules:
# 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

Edit server block:
cd /etc/nginx/sites-available
sudo nano example.com.conf
Add to server block:
include /etc/nginx/includes/fastcgi_cache_excludes.conf;
add_header X-FastCGI-Cache $upstream_cache_status;
Add to PHP location block:
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache NAME;
fastcgi_cache_valid 60m;

Step 4: Enable Cache Purging

Add purge location to server block:
location ~ /purge(/.*) {
fastcgi_cache_purge NAME "$scheme$request_method$host$1";
}

Step 5: Test and Reload

Test configuration and reload:
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl restart php8.3-fpm

Step 6: Verify Cache is Working

Check cache headers:
curl -I https://example.com
curl -I https://www.example.com
Success Indicator: Look for X-FastCGI-Cache: HIT in the response headers. First request will show MISS, subsequent requests should show HIT.

2.3 Removing FastCGI Cache

Edit server block:
cd /etc/nginx/sites-available
sudo nano example.com.conf

Remove or comment out the following directives:

Reload services:
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl restart php8.3-fpm

3. Object Caching (Redis)

3.1 Understanding Object Caching

Object caching stores the results of database queries in memory, eliminating the need to query the database repeatedly for the same data. This is particularly beneficial for logged-in users who cannot benefit from page caching.

Object Cache Architecture

WordPress Query
Check Redis Cache
Cache Hit? → Return from Memory
Cache Miss? → Query Database
Store in Redis & Return
Key Benefits:
  • Reduces database load significantly
  • Faster response times for logged-in users
  • Persistent cache across page loads
  • Handles complex database queries efficiently

3.2 Installing and Configuring Redis

Step 1: Install Redis and PHP Extension

Update and install packages:
sudo apt update
sudo apt install redis-server php8.3-redis

Step 2: Verify Redis Installation

Check Redis status:
sudo systemctl status redis-server
Check Redis logs:
sudo cat /var/log/redis/redis-server.log

Step 3: Fix Memory Overcommit Warning

If you see a warning about overcommit_memory in the logs, fix it as follows:

Create sysctl configuration:
cd /etc/sysctl.d/
sudo nano 11-redis.conf
Add this line:
vm.overcommit_memory = 1
Reboot system:
sudo reboot

Step 4: Configure Redis Memory Settings

Edit Redis configuration:
cd /etc/
sudo nano redis/redis.conf
Add/modify these lines:
maxmemory 256mb
maxmemory-policy allkeys-lru
Memory Policy Explanation: allkeys-lru means Redis will remove the least recently used keys when memory limit is reached, keeping the most frequently accessed data in cache.
Restart Redis:
sudo systemctl restart redis-server
sudo reboot

3.3 Configure WordPress for Redis

Step 1: Set Cache Key Salt

Edit wp-config.php:
cd /var/www/example.com/public_html/
sudo nano wp-config.php
Add this line (before "That's all, stop editing!"):
define( 'WP_CACHE_KEY_SALT', 'example.com' );
Important: Use your actual domain name as the salt value to ensure cache keys are unique to your site.

Step 2: Configure WooCommerce Exclusions (if applicable)

If you're running WooCommerce, prevent Redis from caching session data:

Add to wp-config.php:
/** PREVENT REDIS CACHING WOO SESSION DATA */
define('WP_REDIS_IGNORED_GROUPS', 'wc_session');
WooCommerce Cache Exclusions: Make sure to exclude these pages and cookies from caching:
  • Pages: /cart/, /my-account/, /checkout/
  • Cookies: woocommerce_cart_hash, woocommerce_items_in_cart, wp_woocommerce_session_

Step 3: Install Redis Object Cache Plugin

Install the "Redis Object Cache" plugin from the WordPress repository and activate it. Then click the "Enable Object Cache" button in the plugin settings.

4. OPcode Caching (OPcache)

4.1 Understanding OPcode Cache

OPcode caching stores precompiled PHP script bytecode in memory. When PHP code is executed, it's first compiled into opcode (machine-readable instructions). Without OPcache, this compilation happens every time a script runs. With OPcache enabled, the compiled bytecode is stored in shared memory, bypassing the compilation step for subsequent requests.

OPcode Cache Process

PHP Script Request
Check OPcache
Bytecode Cached? → Execute
Not Cached? → Compile PHP
Store Bytecode & Execute

4.2 Configuring OPcache

Step 1: Navigate to PHP-FPM Pool Configuration

Change to pool directory:
cd /etc/php/8.3/fpm/pool.d/
ls

Step 2: Edit Pool Configuration File

Open your site's pool config:
sudo nano example.com.conf

Step 3: Add OPcache Directives

Add the following directives to the end of your pool configuration file:

DEVELOPMENT SERVER Configuration:
; OPCACHE CONFIGURATION - DEVELOPMENT SERVER - Jul 2025
; Directive php_admin_flag[opcache.enabled] leave commented - enabled by default
; 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 (commented out initially):
; OPCACHE CONFIGURATION - PRODUCTION SERVER - Jul 2025
; Directive php_admin_flag[opcache.enabled] leave commented - enabled by default
;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
Directive Purpose Recommended Value
memory_consumption Memory allocated for OPcache 256 MB
interned_strings_buffer Memory for storing interned strings 32 MB
max_accelerated_files Maximum number of PHP files to cache 20000 (adjust based on file count)
validate_timestamps Check file modification times 1 (dev), 0 (production)
revalidate_freq How often to check timestamps (seconds) 2 seconds
Key Difference: The main difference between development and production configurations is validate_timestamps. Set to 1 for development (checks for file changes) and 0 for production (maximum performance).

Step 4: Reload PHP-FPM

Apply changes:
sudo systemctl reload php8.3-fpm

4.3 Monitoring OPcache File Count

Count PHP Files in Your Installation

Change to web root:
cd /var/www/
Count all PHP files:
sudo find . -type f -print | grep php | wc -l
Understanding max_accelerated_files:
  • The value must be higher than your actual PHP file count
  • PHP uses the first prime number ≥ your configured value
  • Configured value of 20,000 → Actual value: 32,531 (next prime)
  • Check this value after installing themes, plugins, or WordPress updates
  • A single site with minimal plugins: ~1,500-2,000 files
  • Multiple sites with commercial themes: 20,000-30,000+ files
Action Required: If your PHP file count approaches 25,000-30,000, increase max_accelerated_files to 40,000. The actual cache size will automatically adjust to 65,537 (next prime number).

Verify Current OPcache Setting

Display current configuration:
cd /etc/php/8.3/fpm/pool.d/
sudo cat example.com.conf

5. Browser Caching

5.1 Understanding Browser Caching

Browser caching allows web browsers to store copies of web resources (HTML pages, CSS stylesheets, JavaScript files, images, and other media files) locally on a user's device. When a user visits your site, their browser saves certain elements in its cache. On subsequent visits, the browser retrieves these elements from local storage instead of downloading them again from the server.

Benefits of Browser Caching:
  • Faster page load times for returning visitors
  • Reduced server bandwidth consumption
  • Improved user experience with smooth browsing
  • Lower server resource usage

5.2 Implementation

Browser caching is typically configured through NGINX server blocks using HTTP headers that specify how long browsers should cache different types of files. This configuration is usually set up when creating your NGINX server blocks and includes directives for various file types (images, CSS, JavaScript, fonts, etc.).

Note: Browser caching configuration is implemented in NGINX server blocks during initial setup. The cache duration is specified using expires directives and Cache-Control headers for different file types.

6. WordPress Caching Plugin Configurations

6.1 WP Super Cache

Step 1: Create Cache Exclusion Rules

Create WP Super Cache exclusion file:
cd /etc/nginx/includes/
sudo nano wp_super_cache_excludes.conf
Add exclusion rules:
# 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';
}

Step 2: Configure Location Block

Add to exclusion file:
# 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 3: Include in Server Block

Edit server block:
cd /etc/nginx/sites-available/
sudo nano example.com.conf
Add include directive:
include /etc/nginx/includes/wp_super_cache_exclusions.conf;
Reload NGINX:
sudo nginx -t
sudo systemctl reload nginx

Uninstalling WP Super Cache

Remove include directive:
cd /etc/nginx/sites-available/
sudo nano example.com.conf

Remove: include /etc/nginx/includes/wp_super_cache_exclusions.conf;

Uncomment the default location block:

location / {
try_files $uri $uri/ /index.php$is_args$args;
}
Reload services:
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl reload php8.3-fpm

6.2 W3 Total Cache (W3TC)

Step 1: Prepare nginx.conf File

Create nginx.conf in public_html:
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
Security: Replace username:username with your actual username. This file will be automatically populated by W3TC.

Step 2: Secure nginx.conf File

Add to security directives:
cd /etc/nginx/includes/
sudo nano nginx_security_directives.conf
Add this line:
location = /nginx.conf { deny all; }

Step 3: Create W3TC Exclusion Rules

Create W3TC exclusion file:
cd /etc/nginx/includes/
sudo nano w3tc_cache_excludes.conf
Add exclusion rules:
# ---------------------
# 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 4: Configure Server Block

Edit server block:
cd /etc/nginx/sites-available/
sudo nano example.com.conf

Comment out the default location block and add includes:

#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 5: Install PHP Tidy Extension (for minification)

Install and reload:
sudo apt update
sudo apt install php8.3-tidy
sudo systemctl reload nginx
sudo systemctl reload php8.3-fpm

Uninstalling W3 Total Cache

Edit server block:
cd /etc/nginx/sites-available/
sudo nano example.com.conf

Uncomment the default location block and remove includes:

location / {
try_files $uri $uri/ /index.php$is_args$args;
}

Remove these lines:

Remove cache directories:
cd /var/www/example.com/public_html/wp-content/
sudo rm -rf cache/ w3tc-config/
Remove nginx.conf:
cd /var/www/example.com/public_html/
sudo rm nginx.conf
Reload services:
sudo systemctl reload php8.3-fpm
sudo systemctl reload nginx

7. Additional Optimizations

7.1 WordPress Post Revisions

Disable or limit post revisions to reduce database size:

Edit wp-config.php:
cd /var/www/example.com/
sudo nano public_html/wp-config.php
Add before "That's all, stop editing!":
define('WP_POST_REVISIONS', 'false');
Restart PHP-FPM:
sudo systemctl restart php8.3-fpm

7.2 WordPress Memory Limit

PHP-FPM Pool Configuration

Edit pool configuration:
cd /etc/php/8.3/fpm/pool.d/
sudo nano example.com.conf
Set memory limit (uncomment and modify):
;php_admin_value[memory_limit] = 32M
php_admin_value[memory_limit] = 256M

WordPress Configuration

Add to wp-config.php:
/** MEMORY LIMIT */
define('WP_MEMORY_LIMIT', '256M');

7.3 WordPress Cron Management

Disable WP-Cron and use system cron for better performance:

Disable WP-Cron in wp-config.php:
define('DISABLE_WP_CRON', true);
Restart PHP-FPM:
sudo systemctl restart php8.3-fpm
Add system cron job:
crontab -e
Add this job (runs every 15 minutes):
*/15 * * * * wget -q -O - https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

7.4 PHP-FPM Process Manager Configuration

Calculate Average Memory per Process

Display pool usernames:
grep -E '^\s*user\s*=' /etc/php/8.3/fpm/pool.d/*.conf | awk -F= '{print $2}' | xargs | tr ' ' '\n' | sort -u
Calculate average memory (replace POOL_USER with 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") }'

Configure OnDemand Process Manager

Edit pool configuration:
cd /etc/php/8.3/fpm/pool.d/
sudo nano example.com.conf
Add/modify these directives:
pm = ondemand
pm.max_children = as calculated
pm.process_idle_timeout = 10s;
pm.max_requests = 500
Reload PHP-FPM:
sudo systemctl reload php8.3-fpm

Monitor for max_children Warnings

Check PHP-FPM logs:
ls /var/log/
sudo grep max_children /var/log/php8.3-fpm.log
Warning Message: If you see "WARNING: [pool www] server reached max_children setting (25), consider raising it" in your logs, increase the pm.max_children value.

7.5 Cloudflare Integration

If using Cloudflare, configure NGINX to recognize real client IP addresses:

Create Cloudflare IP list:
cd /etc/nginx/includes
sudo nano cloudflare_ip_list.conf
Add Cloudflare IP ranges:
# Last updated OCT 2022
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;
Include in server block:
cd /etc/nginx/sites-available/
sudo nano example.com.conf
Add include directive:
include /etc/nginx/includes/cloudflare_ip_list.conf;
Reload NGINX:
sudo nginx -t
sudo systemctl reload nginx
Update Regularly: Cloudflare IP ranges can change. Check for updates at:
Cloudflare Auto-Minify Deprecation: Note that Cloudflare has deprecated the Auto-Minify feature. Reference: Cloudflare Community Announcement

Summary

This comprehensive guide has covered all essential caching strategies for optimizing WordPress performance on NGINX servers:

Implementation Checklist:

  • Page Caching - FastCGI Cache for maximum performance
  • Object Caching - Redis for persistent database query caching
  • OPcode Caching - OPcache for compiled PHP bytecode
  • Browser Caching - NGINX headers for static asset caching
  • Plugin Configuration - WP Super Cache and W3 Total Cache
  • Additional Optimizations - Memory limits, WP-Cron, PHP-FPM tuning
Best Practices:
  • Always test configuration changes with sudo nginx -t before reloading
  • Monitor your PHP file count regularly and adjust OPcache settings
  • Use development settings while building, switch to production settings when live
  • Regularly update Cloudflare IP lists if using Cloudflare
  • Monitor PHP-FPM logs for max_children warnings
  • Keep Redis memory settings appropriate for your server resources

By implementing these caching strategies, you'll achieve significant performance improvements, reduced server load, and faster response times for your WordPress sites.