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.
-
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). -
If
wp-config.phpis 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.
- If
wp-config.phpis read-only (for example on managed hosting), add the constant by hand above the/* That's all, stop editing! */line:
define( 'WP_CACHE', true );
- The drop-in is installed to
wp-content/advanced-cache.phpautomatically whenfpc_enabledis 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 Cache — WP_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 sanitizedHTTP_HOST(port included when non-default).{path}is the URL path with any character outside[A-Za-z0-9._/-]replaced with_.index.htmlis the rendered HTML the browser receives.index.html_gzipis the pre-gzipped sibling — nginxgzip_static oncan serve it directly for clients sendingAccept-Encoding: gzip.index.metais 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 asX-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.xsl→X-Cache-Bypass: EXT - Paths
/robots.txtor/.htaccess→X-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, orprivate. - Response
Content-Typeis nottext/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
Ageheader your visitors see comes from Varnish, not the FPC. If you want to know whether the FPC served a request directly, look atX-Cache: HIT*(set by the drop-in, not by Varnish). - TTLs should be coordinated —
cache_ttl_defaultcontrols FPC freshness; Varnish'sberesp.ttlcontrols upstream freshness. When the FPC purges, the next request rebuilds and Varnish caches the fresh response. - The drop-in always emits
Vary: Accept-Encodingso 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: MISSrepeatedly — the buffer-level check is rejecting the response. Look forCache-Control: no-cacheheaders 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.