🚀 NGINX FastCGI Caching

Complete Configuration Guide for WordPress Optimization

📋 Introduction to FastCGI Caching

FastCGI caching for WordPress dramatically enhances site performance by caching dynamic content generated by PHP scripts. This reduces server load and database queries, leading to faster page load times and improved user experience.

💡 Key Benefits

  • Performance Enhancement: Significantly reduces page load times by serving cached content
  • Reduced Server Load: Minimizes PHP processing and database queries
  • Robust Architecture: Continues serving content even if PHP-FPM crashes
  • Scalability: Handles high traffic volumes efficiently

🔄 FastCGI Caching Flow

Client Request
NGINX
Cache Check
Response

If cache exists: Serve from cache (HIT) | If cache missing: Query PHP-FPM (MISS)

⚠️ Understanding the Permissions Challenge

Due to site hardening implementations, WordPress caching plugins cannot delete server-side FastCGI cache files. This occurs because:

  • The site runs as a different user than the web server
  • Cached files created by NGINX are owned by the web server user
  • The PHP pool user, although added to the web server group, has no permissions on cached files

⚡ Permission Issue Breakdown

ls -la /var/run/

Directory Ownership: Owner: www-data | Group: root

Permissions: rwx------ (Only owner has read/write/execute)

This makes it impossible for WordPress plugins to clear the cache, as the group has no permissions on the files.

🔧 Solutions Available

  • Command-line purging: Enable selective purging via bash scripts
  • Alternative approach: Use caching plugins like WP Super Cache or W3 Total Cache
  • Client sites: Recommended to use caching plugins for easier administration

⚙️ FastCGI Cache Configuration

Step 1: Configure HTTP Context in nginx.conf

First, edit the main NGINX configuration file:

cd /etc/nginx
sudo nano nginx.conf

Position your cursor above the # Virtual Host Configs comment and add the following directives:

### FASTCGI CACHING # fastcgi_cache_path directive - PATH & NAME must be unique for each site # Add a new fastcgi_cache_path for each site and give a new keys_zone name fastcgi_cache_path /var/run/SITE levels=1:2 keys_zone=NAME: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 Configuration Notes

Directive Description Example Value
Path Location of cache storage (must be unique per site) /var/run/example
levels Subdirectory structure for cache files 1:2
keys_zone Unique name to identify cache memory zone NAME:100m
inactive Time before unused cache is removed 60m

📝 Example Configuration for Your First Site

Replace SITE with your site identifier (e.g., "example") and NAME with a unique cache zone name:

fastcgi_cache_path /var/run/example levels=1:2 keys_zone=example: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;

Test the configuration (do NOT reload yet):

sudo nginx -t

Step 2: Create Cache Exclusion Rules

Create a file to define which content should not be cached:

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

Add the following 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 your site's NGINX server block configuration:

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

Add the include directive near the top of the server block:

include /etc/nginx/includes/fastcgi_cache_excludes.conf; add_header X-FastCGI-Cache $upstream_cache_status;

In the PHP processing location block, add FastCGI caching directives:

location ~ \.php$ { # ... existing PHP configuration ... # FASTCGI CACHING DIRECTIVES fastcgi_cache_bypass $skip_cache; fastcgi_no_cache $skip_cache; fastcgi_cache NAME; fastcgi_cache_valid 60m; # ... rest of PHP configuration ... }

⚠️ Important: Replace NAME with the exact keys_zone name you specified in nginx.conf (e.g., "example").

Step 4: Test and Enable Configuration

Test the NGINX configuration:

sudo nginx -t

If the test is successful, reload NGINX:

sudo systemctl reload nginx

🔍 Verifying Cache Status

Use the curl command to check if FastCGI caching is working properly:

curl -I https://example.com

Look for the X-FastCGI-Cache header in the response. You will receive one of three statuses:

Cache Status Responses

Status Description Meaning
HIT Content served from cache Page was found in cache and served directly by NGINX
MISS Cache not found Page wasn't cached and was generated by PHP-FPM
BYPASS Cache intentionally skipped Page matched exclusion rules (e.g., /wp-admin/)

Testing Cache Behavior

Test 1: Homepage

curl -I https://example.com

First request: X-FastCGI-Cache: MISS

Second request: X-FastCGI-Cache: HIT

Test 2: Admin Area (Should Bypass)

curl -I https://example.com/wp-admin/

Expected result: X-FastCGI-Cache: BYPASS

Test 3: Individual Post

curl -I https://example.com/test-post-1/

First request: X-FastCGI-Cache: MISS

Second request: X-FastCGI-Cache: HIT

✅ Expected Workflow

  1. Create a new WordPress post
  2. First curl request to post URL returns MISS
  3. Second curl request returns HIT
  4. Subsequent requests continue to return HIT

🗑️ Selective Cache Purging

Add a location block to enable manual cache purging for specific URLs:

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

Add the following location block inside your server block:

location ~ /purge(/.*) { fastcgi_cache_purge NAME "$scheme$request_method$host$1"; }

Note: Replace NAME with your keys_zone name. You can use any path for purging (e.g., /purge/, /clear/, etc.).

Test and reload:

sudo nginx -t
sudo systemctl reload nginx

How to Purge Specific Pages

To purge a specific page from cache, visit:

https://example.com/purge/test-post-1/

To purge the homepage:

https://example.com/purge/

🔧 Additional WordPress Optimizations

Post Revisions Management

Disable or limit post revisions to reduce database bloat:

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

Add before the "That's all, stop editing!" line:

// Disable post revisions define('WP_POST_REVISIONS', false); // Or limit to specific number // define('WP_POST_REVISIONS', 3);
sudo systemctl restart php8.3-fpm

Memory Limit Configuration

Increase PHP memory limit for WordPress:

sudo nano /etc/php/8.3/fpm/pool.d/example.com.conf

Add or modify:

php_admin_value[memory_limit] = 256M

Also add to wp-config.php:

/** MEMORY LIMIT */ define('WP_MEMORY_LIMIT', '256M');

WP-Cron Optimization

Disable default WP-Cron and use system cron:

sudo nano /var/www/example.com/public_html/wp-config.php
define('DISABLE_WP_CRON', true);
sudo systemctl restart php8.3-fpm

Set up system cron job:

crontab -e

Add the following line (runs every 15 minutes):

*/15 * * * * wget -q -O - https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

⚡ OPcache Configuration

OPcache improves PHP performance by storing precompiled script bytecode in memory:

cd /etc/php/8.3/fpm/pool.d/
sudo nano example.com.conf

Development Server Configuration

; OPCACHE CONFIGURATION - DEVELOPMENT SERVER ; Directive php_admin_flag[opcache.enabled] leave commented - enabled by default 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 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: Enable (1) for development, disable (0) for production
  • revalidate_freq: Only needed in development (not used in production)
  • Production: Disabling timestamp validation provides maximum performance

Calculate appropriate max_accelerated_files value:

cd /var/www/
sudo find . -type f -print | grep php | wc -l

Set max_accelerated_files to at least the number of PHP files (rounded up to nearest prime).

Apply changes:

sudo systemctl reload php8.3-fpm

🔄 PHP-FPM Process Management

Calculate Average Memory Usage

First, 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 per process (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") }'

OnDemand Process Manager Configuration

cd /etc/php/8.3/fpm/pool.d/
sudo nano example.com.conf
pm = ondemand pm.max_children = 25 ; Calculate based on available memory pm.process_idle_timeout = 10s pm.max_requests = 500

💡 Calculation Formula

max_children = (Available RAM) / (Average Process Memory)

Example: 2GB RAM / 40MB per process = 50 max children (leave room for other processes)

sudo systemctl reload php8.3-fpm

Monitor for warnings:

sudo grep max_children /var/log/php8.3-fpm.log

If you see warnings about reaching max_children, increase the value accordingly.

☁️ Cloudflare Integration

Configure NGINX to properly handle requests proxied through Cloudflare:

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

Add Cloudflare IP ranges (update regularly from https://www.cloudflare.com/ips/):

# Cloudflare IPv4 Ranges 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; # Cloudflare IPv6 Ranges 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 your server block:

sudo nano /etc/nginx/sites-available/example.com.conf
include /etc/nginx/includes/cloudflare_ip_list.conf;
sudo nginx -t
sudo systemctl reload nginx

❌ Removing FastCGI Cache

If you need to disable FastCGI caching:

Step 1: Edit Server Block

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

Remove or comment out the following lines:

# Remove this line: include /etc/nginx/includes/fastcgi_cache_excludes.conf; # Remove or comment: add_header X-FastCGI-Cache $upstream_cache_status; # In PHP location block, comment out: # fastcgi_cache_bypass $skip_cache; # fastcgi_no_cache $skip_cache; # fastcgi_cache NAME; # fastcgi_cache_valid 60m; # Remove purge location: # location ~ /purge(/.*) { # fastcgi_cache_purge NAME "$scheme$request_method$host$1"; # }

Step 2: Test and Reload

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

Note: FastCGI cache directives in nginx.conf can remain as they're only used when called from server blocks.

🔌 Alternative Caching Solutions

WP Super Cache Configuration

Create exclusion rules for WP Super Cache:

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 location / { try_files /wp-content/cache/supercache/$http_host/$cache_uri/index-https.html $uri $uri/ /index.php?$args; }

Include in server block:

cd /etc/nginx/sites-available/
sudo nano example.com.conf
include /etc/nginx/includes/wp_super_cache_excludes.conf;
sudo nginx -t
sudo systemctl reload nginx

W3 Total Cache Configuration

First, create an nginx.conf file in WordPress root:

cd /var/www/example.com/public_html/
sudo touch nginx.conf
sudo chown username:username nginx.conf
sudo chmod 660 nginx.conf

Secure the nginx.conf file:

cd /etc/nginx/includes/
sudo nano nginx_security_directives.conf
location = /nginx.conf { deny all; }

Create W3TC cache exclusions:

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 location / { try_files /wp-content/w3tc/pgcache/$cache_uri/_index.html $uri $uri/ /index.php?$args; }

Update server block:

sudo nano /etc/nginx/sites-available/example.com.conf
# Comment out default location block: #location / { # try_files $uri $uri/ /index.php$is_args$args; #} # Add includes: include /etc/nginx/includes/w3tc_cache_excludes.conf; include /var/www/example.com/public_html/nginx.conf;

Install required PHP extension:

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

🔴 Redis Object Cache

Redis provides object caching for WordPress, reducing database queries:

Installation

sudo apt update
sudo apt install redis-server php8.3-redis

Check status:

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

Fix Overcommit Memory Warning

If you see a warning about overcommit_memory:

cd /etc/sysctl.d/
sudo nano 11-redis.conf
vm.overcommit_memory = 1
sudo reboot

Configure Redis Memory

sudo nano /etc/redis/redis.conf

Find and set:

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

WordPress Configuration

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

Add before "That's all, stop editing!":

/** REDIS CACHE KEY SALT */ define('WP_CACHE_KEY_SALT', 'example.com');

WooCommerce Redis Configuration

For WooCommerce sites, prevent Redis from caching session data:

/** PREVENT REDIS CACHING WOO SESSION DATA */ define('WP_REDIS_IGNORED_GROUPS', 'wc_session');

🛒 WooCommerce Cache Exclusions

Exclude the following pages and cookies from caching:

  • Pages: /cart/, /my-account/, /checkout/
  • Cookies: woocommerce_cart_hash, woocommerce_items_in_cart, wp_woocommerce_session_, woocommerce_recently_viewed, store_notice[notice id]

✅ Best Practices & Tips

Performance Optimization Checklist

Component Recommendation Impact
FastCGI Cache 60m cache validity, 100MB memory High performance, low resource usage
OPcache 256MB, disable timestamps in production Significant PHP performance boost
Redis 256MB, allkeys-lru policy Reduces database queries dramatically
PHP-FPM OnDemand process manager Efficient memory usage

Security Considerations

  • Always run sites as separate users (not www-data)
  • Set appropriate file permissions (660 for config files)
  • Secure sensitive files like nginx.conf in WordPress root
  • Keep Cloudflare IP lists updated regularly
  • Monitor PHP-FPM logs for max_children warnings

Monitoring & Maintenance

sudo systemctl status nginx
sudo systemctl status php8.3-fpm
sudo systemctl status redis-server

Check log files regularly:

sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/php8.3-fpm.log
sudo tail -f /var/log/redis/redis-server.log

Common Issues & Solutions

Issue Cause Solution
Cache always shows MISS fastcgi_cache directive not set or incorrect name Verify keys_zone name matches in nginx.conf and server block
Unable to purge cache via plugin Permission issues Use command-line purging or switch to caching plugin
Server out of memory Too many cache zones or PHP processes Reduce cache sizes or max_children value
Redis overcommit warning Kernel parameter not set Set vm.overcommit_memory = 1 in sysctl

📊 Summary

🎯 Key Takeaways

  1. FastCGI Caching: Provides the fastest caching solution for WordPress, serving content even when PHP is down
  2. Configuration Hierarchy: HTTP context (nginx.conf) → Include files → Server block configuration
  3. Cache Exclusions: Critical to exclude admin areas, logged-in users, and dynamic content
  4. Verification: Use curl -I to check X-FastCGI-Cache header (HIT/MISS/BYPASS)
  5. Alternative Solutions: WP Super Cache and W3 Total Cache offer easier management for client sites
  6. Complementary Technologies: Combine with OPcache and Redis for maximum performance
  7. Security: Proper user separation and file permissions are essential

Complete Performance Stack

NGINX
+
FastCGI Cache
+
OPcache
+
Redis
=
Optimal Performance