Skip to content

Full-Page Cache

A native disk-first full-page cache served before WordPress fully loads. New in v2.1.0. Caches anonymous HTML to wp-content/cache/cacheability-pro/ with a pre-gzipped sibling so nginx can try_files directly without touching PHP, and optionally mirrors writes into Redis using the same connection as the Redis Object Cache.

What it does

WordPress is a per-request templating engine. Every page builds itself from options, posts, terms, widgets, and theme code on every hit. The full-page cache (FPC) saves the rendered HTML on first request and serves it for subsequent anonymous visitors straight off disk — TTFB drops from "however slow your slowest plugin is" to "a single file read".

The drop-in mirrors WP Rocket's bypass semantics so migrating from it is a config change, not a re-learn. The on-disk layout matches WP Rocket and W3TC, so an existing nginx try_files rule keeps working when you switch over.

Defaults

Out of the box, after activating Cacheability Pro and defining WP_CACHE:

Option Default What it controls
fpc_enabled true Master on/off switch for the FPC.
fpc_store_disk true Write cached HTML (+ gzip sibling + meta) to disk.
fpc_store_redis false Also write entries into Redis. Off by default — disk-only is fast enough for most setups.
fpc_esi_on_disk post_process How to handle <esi:include> tags in cached pages. See ESI coexistence.
TTL 1209600 (14 days) Inherited from cache_ttl_default. Disk entries are flushed on content changes, so a long TTL is safe.

The Phase 2 plan deliberately omits a "Never Cache URIs" admin list and mobile cache variants — the bypass catalogue (below) handles the realistic cases without exposing extra knobs.

Enabling

The FPC needs the WordPress WP_CACHE constant. WordPress only loads advanced-cache.php when WP_CACHE is true.

  1. In wp-admin, open Cacheability Pro → Full-Page Cache. The tab reports three states for WP_CACHE: defined and true (green), defined and false (red), or undefined (yellow).

  2. If wp-config.php is writable, click Enable now. Cacheability Pro writes:

define( 'WP_CACHE', true ); // Added by Cacheability Pro

The trailing marker is important — when removing the FPC, only lines carrying that marker are rewritten. A define( 'WP_CACHE', false ); you added by hand is left alone.

  1. If wp-config.php is read-only (for example on managed hosting), add the constant by hand above the /* That's all, stop editing! */ line:
define( 'WP_CACHE', true );
  1. The drop-in is installed to wp-content/advanced-cache.php automatically when fpc_enabled is on. The tab shows "Installed (ours, v1)" on success.

To remove the FPC, switch fpc_enabled off and click Remove drop-in. We only remove a drop-in that still carries the CACHEABILITY_ADVANCED_CACHE_VERSION stamp — a foreign advanced-cache.php left by another plugin is left untouched.

Storage backends

Option What writes happen When to use
fpc_store_disk only index.html, index.html_gzip, index.meta per URL. The default. nginx try_files serves hits directly; PHP never runs on a cache hit.
fpc_store_disk + fpc_store_redis All of the above, plus a Redis SET per URL under the cfpc: key namespace. Multi-node setups where shared cache state across web servers is worth the doubled write cost.
fpc_store_redis only (not supported) Disk is always the source of truth; Redis is an optional mirror.

Redis writes reuse the connection constants from Redis Object CacheWP_REDIS_HOST, WP_REDIS_PORT, WP_REDIS_PASSWORD, etc. The FPC key prefix is cfpc:, or {WP_REDIS_PREFIX}cfpc: when WP_REDIS_PREFIX is set.

On-disk layout

Each cacheable URL maps to three files under wp-content/cache/cacheability-pro/:

wp-content/cache/cacheability-pro/{host}/{path}/index.html
wp-content/cache/cacheability-pro/{host}/{path}/index.html_gzip
wp-content/cache/cacheability-pro/{host}/{path}/index.meta

Where:

  • {host} is the sanitized HTTP_HOST (port included when non-default).
  • {path} is the URL path with any character outside [A-Za-z0-9._/-] replaced with _.
  • index.html is the rendered HTML the browser receives.
  • index.html_gzip is the pre-gzipped sibling — nginx gzip_static on can serve it directly for clients sending Accept-Encoding: gzip.
  • index.meta is an object-injection-safe serialized array containing the HTTP status, content type, and store timestamp the drop-in needs to set response headers on a hit.

When the request has an allow-listed query parameter (lang, s, or permalink_name), an md5(query_string) segment is appended to the path so each variant gets its own entry:

wp-content/cache/cacheability-pro/example.com/blog/index.html
wp-content/cache/cacheability-pro/example.com/blog/8e2a…/index.html   ← ?lang=fr

This layout is exactly what WP Rocket and W3TC write, which is why the nginx snippet below works without modification when migrating from either of them.

nginx integration

Add this above your usual location / { try_files ... } block to short-circuit hits before PHP-FPM runs:

set $cfpc_dir /wp-content/cache/cacheability-pro/$host$uri;
if (-f $cfpc_dir/index.html_gzip) { add_header X-Cache HIT-GZIP; gzip_static on; }

location / {
    try_files $cfpc_dir/index.html $uri $uri/ /index.php?$args;
}

The exact snippet is also shown in the admin tab with $host pre-filled to your site's hostname if you'd rather copy from there.

Apache integration

For Apache servers, the same idea via mod_rewrite — put this in your .htaccess above the existing WordPress block:

RewriteEngine On
RewriteCond %{REQUEST_METHOD} ^(GET|HEAD)$
RewriteCond %{HTTP_COOKIE} !(wordpress_logged_in_|wp-postpass_|comment_author_|wp_woocommerce_session_) [NC]
RewriteCond %{QUERY_STRING} ^$
RewriteCond %{DOCUMENT_ROOT}/wp-content/cache/cacheability-pro/%{HTTP_HOST}%{REQUEST_URI}/index.html -f
RewriteRule .* /wp-content/cache/cacheability-pro/%{HTTP_HOST}%{REQUEST_URI}/index.html [L]

Apache evaluates the cookie + query-string guards before the file test, so logged-in visitors and pages with query parameters fall through to PHP exactly as if the cache file didn't exist.

ESI coexistence

Cacheability Pro's ESI Support lets per-request fragments (a nonce, the cart total, a logged-in greeting) be re-rendered inside an otherwise-cached page. The FPC has two strategies for handling <esi:include> tags it finds in buffered HTML, controlled by fpc_esi_on_disk:

  • post_process (default). Pages containing ESI tags are cached normally, but on every hit the drop-in resolves each <esi:include> tag via a loopback HTTP request before sending bytes to the client. The gzip sibling is intentionally not written for ESI pages — a static gzip would lock in stale resolved content. Hits are signalled as X-Cache: HIT-ESI-RESOLVED.

  • skip. Pages containing ESI tags are never cached. They pass through to WordPress on every request. This is the right choice when ESI fragments are expensive to resolve and the loopback overhead would exceed a full render.

Both strategies pass through pages that contain no ESI tags — there's no penalty for turning ESI on plugin-wide if only a few templates use it.

Bypass rules

The drop-in skips the cache when any of the following holds. These are checked in order; the first match wins and is reported in the X-Cache-Bypass response header.

Always bypass (no toggle):

  • Non-GET / non-HEAD HTTP method → X-Cache-Bypass: METHOD
  • URI under /wp-admin, /wp-login.php, /wp-cron.php, /xmlrpc.php, or /wp-json/X-Cache-Bypass: URI
  • Path ending in .php (other than /index.php), .xml, or .xslX-Cache-Bypass: EXT
  • Paths /robots.txt or /.htaccessX-Cache-Bypass: URI
  • WP-CLI or WP-Cron execution contexts (the drop-in returns early before any cache lookup runs).

Cookie-triggered bypass:

The drop-in checks every request cookie name against this set of prefixes:

Prefix Purpose
wordpress_logged_in_ Logged-in WordPress user.
wp-postpass_ Post-password-protected content.
comment_author_ Visitor has commented (their own moderation queue needs to be visible).
wp_woocommerce_session_ WooCommerce session.
woocommerce_items_in_cart WooCommerce cart has items.
woocommerce_cart_hash WooCommerce cart hash.
edd_items_in_cart Easy Digital Downloads cart.

Any match returns X-Cache-Bypass: COOKIE. WooCommerce cart and checkout pages bypass automatically — no setting required. Caching pages with cart contents would leak per-user state, so this is hard-coded rather than offered as a toggle.

The Additional bypass cookies textarea in the admin tab (option fpc_bypass_cookies) accepts newline-separated cookie-name prefixes that extend the list — useful when a custom auth plugin sets its own session cookie.

Query-string bypass:

Any request whose query string contains a parameter not on the allow list (lang, s, permalink_name) returns X-Cache-Bypass: QUERY. This catches campaign trackers (utm_*, gclid, fbclid) without polluting the cache with one entry per tracker variant.

Buffer-level bypass (after WordPress renders):

These run after PHP has produced a response but before the entry is written:

  • Body smaller than 256 bytes (probably an error).
  • Body has no closing </html> tag.
  • Response carries Cache-Control: no-cache, no-store, or private.
  • Response Content-Type is not text/html.

The response is still sent to the client; only the cache write is suppressed.

Purge model

The FPC drops cached entries from disk and Redis together. Two purge surfaces trigger writes:

Listening to Varnish HTTP Purge. When the Varnish HTTP Purge companion plugin is installed, the FPC subscribes to its hooks:

  • after_purge_url — single-URL purge.
  • after_purge_tags — tag-based purge (sites grouped by content type).
  • after_full_purge — flush everything.

This keeps disk and Redis in sync with whatever Varnish has been told to drop, without re-implementing purge orchestration in two places.

Standalone purge. When Varnish HTTP Purge is not installed, the FPC hooks WordPress directly:

WP hook What it purges
save_post The post's permalink.
deleted_post The post's permalink.
comment_post, wp_set_comment_status The commented post.
switch_theme Everything.
upgrader_process_complete Everything.
deleted_plugin Everything.

A single-URL purge removes the per-URL directory under cache/cacheability-pro/ and the matching Redis key. A full purge wipes the entire cache/cacheability-pro/ tree.

The X-Purge-Method request header is honored — X-Purge-Method: default is the per-URL purge; ban-style purges (regex / tag with no URL list) are skipped because the FPC indexes entries by URL, not by pattern.

Varnish coexistence

The FPC sits behind Varnish in the cache hierarchy. It does not send purges upstream — Varnish HTTP Purge is the component that talks to Varnish, and the FPC only mirrors what it sees on Varnish's local purge hooks.

Practical implications:

  • The Age header your visitors see comes from Varnish, not the FPC. If you want to know whether the FPC served a request directly, look at X-Cache: HIT* (set by the drop-in, not by Varnish).
  • TTLs should be coordinated — cache_ttl_default controls FPC freshness; Varnish's beresp.ttl controls upstream freshness. When the FPC purges, the next request rebuilds and Varnish caches the fresh response.
  • The drop-in always emits Vary: Accept-Encoding so Varnish doesn't serve a gzipped body to a client that didn't ask for one.

Diagnostics

Every response served (or bypassed) carries one of:

Header Meaning
X-Cache: MISS Cacheable, but no entry yet. The response is being written to disk during the request.
X-Cache: HIT Served from disk, uncompressed.
X-Cache: HIT-GZIP Served from the pre-gzipped sibling. Set by nginx when gzip_static on matches index.html_gzip.
X-Cache: HIT-ESI-RESOLVED Served from disk after resolving <esi:include> tags via loopback.
X-Cache: HIT-REDIS Served from Redis (disk miss, Redis hit).
X-Cache-Bypass: <REASON> Bypass reason: METHOD, URI, EXT, COOKIE, QUERY, SSL, SPEEDTOOL. The cache was deliberately skipped.

A quick check from the shell:

curl -sI https://example.com/ | grep -iE 'x-cache|age'

The admin Full-Page Cache tab shows the drop-in's installed status (ours / foreign / missing), the bundled drop-in version, the on-disk cache size, and the current WP_CACHE constant state.

Configuration constants

Constant Default Purpose
WP_CACHE (undefined) Must be true for WordPress to load advanced-cache.php. The admin tab can write this for you.
CACHEABILITY_FPC_DISABLED false Hard kill switch. When defined and true, the drop-in returns immediately and WordPress handles the request as if no cache were installed. Use as an instant rollback if a misbehaving entry is being served.

Troubleshooting

FPC tab shows "Drop-in missing". WP_CACHE is set but wp-content/advanced-cache.php was deleted by another tool. Re-save the Full-Page Cache settings — the installer rewrites the drop-in on every option save.

FPC tab shows "Foreign drop-in". Another plugin has installed advanced-cache.php. Remove that plugin (or its drop-in) before clicking Install drop-in. Cacheability Pro will not overwrite a drop-in it didn't write.

Pages aren't being cached. Check the response headers:

  • X-Cache-Bypass: COOKIE — a cookie in the request matches the bypass list. Test in an incognito window to rule out session cookies.
  • X-Cache-Bypass: QUERY — the URL has a non-allow-listed query parameter. Most campaign trackers fall into this bucket.
  • X-Cache: MISS repeatedly — the buffer-level check is rejecting the response. Look for Cache-Control: no-cache headers your theme or another plugin might be sending.

Cache won't flush. Click Flush full-page cache in the admin tab to wipe the entire tree, or define CACHEABILITY_FPC_DISABLED in wp-config.php to bypass entirely while you investigate.

ESI-resolved pages serve stale fragments. The post_process strategy resolves fragments via loopback on every hit, so the fragment source is re-rendered each time. If you're seeing stale data, the issue is in the fragment endpoint itself, not the FPC entry. Switch to skip while debugging to take the FPC out of the loop.