Full-page caching stores the complete HTML output of a WordPress page so subsequent requests are served without executing PHP or querying MySQL. It is the most impactful single performance optimisation for most WordPress sites — the difference between 10ms and 500ms page response time.
Problem: A WordPress site serves dynamically generated HTML on every request — PHP, database queries, and template rendering run for every page view even when the content rarely changes, causing high TTFB under moderate traffic.
Solution: Implement full-page caching: serve static HTML files from disk for anonymous visitors using a plugin (WP Super Cache, W3 Total Cache) or Nginx fastcgi_cache. Configure cache bypass rules for logged-in users, POST requests, WooCommerce cart/checkout pages, and query string parameters. Purge the cache selectively on post save with the save_post hook.
The examples below compare the four main full-page caching strategies, implement a minimal file-based cache in a must-use plugin, and show the correct approach for bypassing cache for logged-in users and WooCommerce cart pages.
# Full-page cache strategy comparison:
#
# Strategy Where cache lives Miss cost Config difficulty
# ────────────
# Nginx FastCGI Nginx layer Lowest Medium (nginx.conf)
# Varnish Proxy layer Lowest High (VCL)
# WP Super Cache PHP / Apache Low Low (plugin)
# W3 Total Cache PHP / CDN Low Medium (plugin)
# Object Cache Pro Redis (page cache) Medium Low (plugin)
#
# Recommended for VPS/dedicated: Nginx FastCGI cache (fastest)
# Recommended for shared hosting: WP Super Cache or W3 Total Cache
# ── Nginx FastCGI Cache setup ──
# /etc/nginx/nginx.conf (http block):
# fastcgi_cache_path /var/cache/nginx/wordpress
# levels=1:2 keys_zone=WORDPRESS:100m inactive=60m max_size=1g;
# fastcgi_cache_key "$scheme$request_method$host$request_uri";
# /etc/nginx/sites-available/helloadmin.com (server block):
# set $skip_cache 0;
# if ($request_method = POST) { set $skip_cache 1; }
# if ($query_string != "") { set $skip_cache 1; }
# if ($request_uri ~* "/wp-admin/") { set $skip_cache 1; }
# if ($http_cookie ~* "wordpress_logged_in") { set $skip_cache 1; }
# if ($http_cookie ~* "woocommerce_items_in_cart") { set $skip_cache 1; }
#
# fastcgi_cache WORDPRESS;
# fastcgi_cache_valid 200 60m;
# fastcgi_cache_bypass $skip_cache;
# fastcgi_no_cache $skip_cache;
# add_header X-Cache $upstream_cache_status;
Minimal PHP output-buffer page cache as a must-use plugin:
<?php
// wp-content/mu-plugins/page-cache.php
// Minimal file-based page cache — educational; use a plugin in production
define( 'PAGE_CACHE_DIR', WP_CONTENT_DIR . '/cache/pages/' );
define( 'PAGE_CACHE_TTL', 3600 ); // 1 hour
function page_cache_should_serve(): bool {
if ( is_admin() || defined( 'DOING_AJAX' ) || defined( 'DOING_CRON' ) ) return false;
if ( $_SERVER['REQUEST_METHOD'] !== 'GET' ) return false;
if ( ! empty( $_GET ) ) return false;
// Bypass for logged-in users and WooCommerce cart
foreach ( array_keys( $_COOKIE ) as $name ) {
if ( str_starts_with( $name, 'wordpress_logged_in' ) ) return false;
if ( str_starts_with( $name, 'woocommerce_' ) ) return false;
}
return true;
}
function page_cache_file(): string {
$key = md5( 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
return PAGE_CACHE_DIR . substr( $key, 0, 2 ) . '/' . $key . '.html';
}
// Try to serve from cache
if ( page_cache_should_serve() ) {
$file = page_cache_file();
if ( is_readable( $file ) && ( time() - filemtime( $file ) ) < PAGE_CACHE_TTL ) {
header( 'X-Cache: HIT' );
readfile( $file );
exit;
}
}
// Otherwise buffer output and save to cache
add_action( 'template_redirect', function() {
if ( ! page_cache_should_serve() ) return;
ob_start( function( string $html ): string {
$file = page_cache_file();
wp_mkdir_p( dirname( $file ) );
file_put_contents( $file, $html, LOCK_EX );
return $html;
} );
} );
// Purge cache for a post when it's updated
add_action( 'save_post', function( int $post_id ) {
$url = get_permalink( $post_id );
$key = md5( $url );
$file = PAGE_CACHE_DIR . substr( $key, 0, 2 ) . '/' . $key . '.html';
if ( file_exists( $file ) ) unlink( $file );
} );
NOTE: Always bypass the page cache for: logged-in users, POST requests, pages with query strings (except whitelisted SEO parameters), WooCommerce cart/checkout/account pages, and pages with personalised content. A cache that serves logged-in content to anonymous users is worse than no cache at all.