ESI Support (Edge Side Includes)
Cacheability Pro lets an ESI-capable cache, including NGINX with the ESI module or Varnish, cache anonymous page shells while fetching fresh WordPress nonce inputs for each response. Normal premium licensing is required. The feature is disabled by default, and nonce replacement stays inactive until you list at least one audited action.
How nonce ESI works
WordPress nonces depend on the action, user/session and time window. A page cache can otherwise retain a nonce after it expires. With an action on the whitelist and a trusted cache advertising ESI support, Pro:
- Returns a placeholder from
wp_create_nonce()for that action. - Replaces the entire input containing the placeholder with an ESI include.
- Marks the response with
Surrogate-Control: content="ESI/1.0". - Lets the cache retain that shell for its configured TTL and fetch the nonce fragment uncached when assembling each response.
For an audited example action named my_form_action:
<!-- Origin response stored in the shell cache -->
<esi:include src="/?cacheability_esi=nonce&action=bXlfZm9ybV9hY3Rpb24&name=_wpnonce" />
<!-- Assembled response delivered to the browser -->
<input type="hidden" name="_wpnonce" value="a1b2c3d4e5" />
Fresh means generated for the current request, not a different value on every
request. With WordPress defaults, anonymous visitors share a nonce for the
same action during a nonce tick. Sites customizing nonce_user_logged_out or
anonymous sessions need a separate audit of their identity and cache behavior.
Select actions safely
List only actions used exclusively in hidden form inputs. Pro replaces whole input elements. It cannot replace nonces inside URLs, inline JavaScript, localized JSON or data attributes. Audit every use of an action in the theme and plugins, including any client-side form logic, before listing it.
Do not whitelist wp_rest: its nonces commonly appear in localized JavaScript.
Do not assume a plugin-specific action or a broad wildcard is safe merely
because one form uses it. Unsupported uses leave literal placeholders in the
browser and break the affected feature.
Anonymous visitors only. Logged-in users receive real nonces and must bypass the shared page cache. Pro does not provide private per-session page caching. Admin, AJAX, REST, cron, CLI and non-GET/HEAD requests also retain real nonces.
Plugin setup
- Configure and validate the ESI cache as described below before enabling replacement. Never let a client choose the surrogate capability header.
- Activate normally licensed Pro. Activation installs
wp-content/mu-plugins/cacheability-nonce-esi.php. If the directory is not writable, the settings page provides a manual installation snippet. - Open Cacheability Pro → Cache Controls & Policies → ESI Support and enable nonce ESI.
- Enter audited action names, one per line.
*is supported as a wildcard; every matching action still needs an audit. An empty list disables nonce replacement. Other actions keep their real WordPress nonces.
ESI-only integration
To retain an existing site's other cache/header behavior, define
CACHEABILITY_PRO_ESI_ONLY as boolean true in wp-config.php before plugins
load. Use compatible free Cacheability if it supplies the site's existing
behavior. Normal premium licensing, the ESI setting, action list and trusted
capability still apply. This mode does not initialize Pro's unrelated rewriting,
page-cache or warming features. Before switching an existing full Pro install,
use the normal controls to disable/remove its page-cache drop-in and warming
schedules, then deactivate Pro before changing its mode. The constant cannot
stop an earlier advanced-cache.php.
Disable independent full comment-form ESI when validating only nonce inputs:
add_filter( 'cacheability_pro_esi_comment_form', '__return_false' );
Cache and delivery requirements
These requirements apply to both NGINX and Varnish:
- Replace incoming
Surrogate-Capabilitywith a trusted ESI capability only on routes whose responses actually pass through ESI processing. Clear it on other WordPress routes and prevent untrusted access to the backend. - Bypass nonce fragments, authenticated traffic and sensitive routes. Honor upstream cache prevention and Set-Cookie; cache only eligible page shells.
- Deliver assembled nonce-bearing HTML with
Cache-Control: no-store. Apply this at delivery without disabling the internal shell cache. Prevent conditional 304 responses from allowing an old assembled nonce to be reused. - Check CDN rules as well as headers. Existing Cache Everything/TTL overrides can retain assembled HTML. Bypass dynamic HTML and invalidate old HTML objects at cutover, while keeping static CSS, JavaScript and images cacheable.
- Verify complete HTML with no ESI tags or placeholders, valid nonce inputs on shell HITs, login/admin behavior, real form behavior and scoped purges.
NGINX integration
Use ABI-matched nginx-module-esi
and, for native invalidation, nginx-module-cache-purge packages. Legacy
PageSpeed requires ESI 1.0.2 or later, which fixes the shared buffering-bit
conflict. Use cache-purge 2.6.1 or later
when the purge consumer verifies Cache-Purge-Result. Headers-more provides
the request-header clearing used by the tested configuration.
The NGINX integration tests exercise both HTTP proxy and FastCGI caches against real WordPress. Maintainers with source-repository access can inspect the fixtures. Production integration must preserve these contracts:
- Preserve distinct permalink keys across WordPress's
/index.phprewrite. FastCGI uses the original request URI; fragment queries must bypass caching. - Use
volatilebypass maps so ESI subrequests evaluate their own URI/arguments instead of reusing the parent page's decision. Check both original and rewritten paths for sensitive routes. - Enable
esi onon the cached PHP path and inject trusted capability there. ClearIf-None-MatchandIf-Modified-Sincebefore core conditional handling, including on cache HITs. Strip upstream compression where needed so ESI can process the HTML. - Serve static files directly. Keep
open_file_cache offon cached PHP while retaining the PHP existence guard: open descriptors can otherwise keep a deleted cache object serving after a confirmed purge. - Restrict PURGE to an explicitly trusted source. Native tag purges use response
X-Cache-Tagsand requestX-Cache-Tags-Pattern; verify a positiveCache-Purge-Resultand target MISS with an untouched control HIT.
PageSpeed needs additional integration checks. Retain the correct public HTTPS
identity for resource fetching, keep its optimized assets working, and prevent
ModifyCachingHeaders from replacing the assembled response's no-store policy.
The Pro test fixture does not contain PageSpeed; it cannot prove a production
filter chain. Run configuration validation, full rendered-config analysis and
live response/asset checks before cutover, with a per-site rollback and soak.
Varnish integration
The bundled vcl/cacheability-pro.vcl supplies ESI capability and fragment
handling. The settings page also exposes it under Varnish VCL snippet.
This excerpt shows those parts; complete the delivery policy above separately:
sub vcl_recv {
set req.http.Surrogate-Capability = {"varnish="ESI/1.0""};
if (req.url ~ "[?&]cacheability_esi=") {
return (pass);
}
}
sub vcl_backend_response {
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
unset beresp.http.Surrogate-Control;
set beresp.do_esi = true;
set beresp.do_gzip = true;
}
}
Adding actions via a filter
For an action whose complete usage you have audited:
add_filter( 'cacheability_pro_esi_nonce_actions', function ( $actions ) {
$actions[] = 'my_form_action';
return $actions;
} );
Filter values are merged with the admin textarea.
Troubleshooting
- No replacement: check normal premium activation, the ESI setting, action whitelist, installed MU-plugin and trusted capability from the actual cache.
- MU-plugin not installed: use the settings page's manual snippet when
wp-content/mu-plugins/is not writable. - Literal placeholders: remove the affected action and inspect all its rendering contexts before enabling it again.
- REST cookie nonce errors: remove
wp_restfrom the list; localized JSON is not a supported replacement context. - Stale forms despite shell HITs: verify fragment bypass and current nonce validity, then inspect browser/CDN caching and conditional responses for the assembled HTML.