Nginx’s FastCGI cache stores the full HTML response from PHP-FPM on disk and serves subsequent requests for the same URL directly from the cached file, bypassing PHP, WordPress, and the database entirely — reducing Time to First Byte (TTFB) from 200–800ms (PHP execution) to under 5ms (disk read) for cached pages. Unlike page caching plugins (W3 Total Cache, WP Super Cache) that write cache files from within WordPress, FastCGI cache operates at the web server level and is unaware of WordPress internals, which means cache invalidation must be handled explicitly: a WordPress plugin sends a PURGE request to Nginx when a post is published or updated, and Nginx is configured to honor PURGE requests from localhost. The FastCGI cache configuration requires defining a cache zone with fastcgi_cache_path (specifying the disk path, key zone name and size, maximum cache size, and inactive TTL), defining the cache key with fastcgi_cache_key (usually a combination of scheme, host, request URI, and cookie state to ensure logged-in users and anonymous users get different cached versions), and adding the fastcgi_cache directive to the WordPress location block. Critical bypass conditions must be set to prevent caching admin pages, cart/checkout pages, and responses for logged-in users: fastcgi_cache_bypass and fastcgi_no_cache directives check custom variables populated by map blocks that inspect the Cookie header for WordPress login cookies and WooCommerce cart cookies. The $upstream_cache_status variable (values: HIT, MISS, BYPASS, EXPIRED) can be added to a response header (add_header X-Cache-Status $upstream_cache_status) for debugging. Microcaching — setting a 60-second TTL — is a pragmatic middle ground for high-traffic sites with frequently updated content: even a 1-minute cache absorbs most traffic spikes while keeping content reasonably fresh. The systemd timers post covers server-side scheduling; FastCGI caching covers the server-side request handling performance layer.
Problem: A WordPress news site receives 500 simultaneous visitors during a breaking news event — PHP-FPM saturates with 50 worker processes, database connections queue up, and TTFB climbs to 8 seconds, causing timeouts and 502 errors even though each page is identical for all anonymous visitors.
Solution: Enable Nginx FastCGI cache with a 10-minute TTL for anonymous requests, bypass cache for logged-in users and WooCommerce sessions, and add a WordPress plugin that sends PURGE requests to Nginx when posts are published or updated.
# /etc/nginx/nginx.conf — add inside http {} block
fastcgi_cache_path /var/cache/nginx/wordpress
levels=1:2
keys_zone=wordpress_cache:100m # 100MB key zone in shared memory
max_size=2g # max disk usage for cached content
inactive=60m # remove entries not accessed in 60 min
use_temp_path=off; # write directly to cache dir (faster)
# /etc/nginx/sites-available/example.com
server {
listen 443 ssl http2;
server_name example.com www.example.com;
root /var/www/html;
index index.php;
# ── Determine if request should bypass cache ─────────────────────────
set $skip_cache 0;
# Bypass for POST requests (form submissions, AJAX)
if ($request_method = POST) { set $skip_cache 1; }
# Bypass for query strings (except UTM params — we handle below)
if ($query_string != "") { set $skip_cache 1; }
# Bypass for WordPress admin and login
if ($request_uri ~* "/wp-admin/|/wp-login.php") { set $skip_cache 1; }
# Bypass for logged-in users (WordPress sets these cookies on login)
if ($http_cookie ~* "wordpress_logged_in_|wp-postpass_") { set $skip_cache 1; }
# Bypass for WooCommerce active sessions
if ($http_cookie ~* "woocommerce_items_in_cart|woocommerce_cart_hash") { set $skip_cache 1; }
# ── Main WordPress location block ────────────────────────────────────
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# FastCGI cache settings
fastcgi_cache wordpress_cache;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 301 302 10m; # cache 200/301/302 for 10 min
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_use_stale error timeout updating; # serve stale on PHP error
# Expose cache status in response header (for debugging)
add_header X-Cache-Status $upstream_cache_status;
}
# ── PURGE endpoint (only allow from localhost) ────────────────────────
location ~ /purge(/.*) {
allow 127.0.0.1;
deny all;
fastcgi_cache_purge wordpress_cache "$scheme$request_methodGET$host$1";
}
}
// WordPress plugin: purge Nginx cache when post is published/updated
add_action( 'save_post', 'myplugin_nginx_purge_post', 10, 1 );
add_action( 'comment_post','myplugin_nginx_purge_post', 10, 1 );
function myplugin_nginx_purge_post( int $post_id ): void {
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) return;
$url = get_permalink( $post_id );
$purge_url = str_replace( home_url(), 'http://127.0.0.1', $url );
$response = wp_remote_request( $purge_url, [
'method' => 'PURGE',
'timeout' => 3,
'headers' => [ 'Host' => wp_parse_url( home_url(), PHP_URL_HOST ) ],
] );
if ( is_wp_error( $response ) ) {
error_log( 'Nginx PURGE failed for ' . esc_url_raw( $url ) . ': ' . $response->get_error_message() );
}
// Also purge the homepage and archives
myplugin_nginx_purge_url( home_url( '/' ) );
}
NOTE: The fastcgi_cache_purge directive requires the ngx_cache_purge module — it is not included in the standard Nginx package. On Ubuntu/Debian, install nginx-extras (apt install nginx-extras) which includes this module. Alternatively, implement cache invalidation by deleting the cached file directly from the filesystem: the cache key is an MD5 hash of the cache key string, stored at a path derived from the levels setting, and a PHP script can compute the path and unlink() the file without requiring the purge module.