WordPress Full-Page Caching: Strategies and Implementation

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.

Leave Comment

Your email address will not be published. Required fields are marked *