WP Engine EverCache Internals: How WordPress Caching Actually Works Under the Hood
When you deploy a WordPress site on WP Engine, your pages load fast. Suspiciously fast. A site that once took 1.8 seconds to render on shared hosting suddenly returns full HTML in 50 milliseconds. The database queries that used to pile up on every page load simply vanish from the query log. Something is intercepting requests before WordPress even wakes up.
That something is EverCache, WP Engine’s proprietary caching system. But calling it a “caching system” understates what it actually does. EverCache is a multi-layered architecture that combines reverse proxy caching, object caching, CDN distribution, and intelligent cache invalidation into a single coordinated system. Understanding how each layer works, and how they interact, is the difference between a site that flies and one that fights its own hosting infrastructure.
I have spent the better part of four years helping clients optimize their WP Engine deployments. This article documents what I have learned about how EverCache actually functions at each layer, how to debug it when things go wrong, and how to avoid the mistakes that cost sites their performance edge.
The Architecture: What Sits Between the Browser and WordPress
Before a single line of PHP executes, your request passes through multiple layers of infrastructure. Understanding this stack is fundamental to understanding cache behavior on WP Engine.
Layer 1: The CDN Edge (Cloudflare or Global Edge Security)
WP Engine offers CDN services through their Global Edge Security product (powered by Cloudflare) or through their legacy MaxCDN-based CDN. When a request arrives, it first hits the CDN edge node closest to the visitor. If the edge node has a cached copy of the requested resource, it returns that copy immediately. The request never reaches WP Engine’s origin servers at all.
For static assets like CSS, JavaScript, images, and fonts, CDN cache hit rates typically exceed 95% on well-configured sites. For HTML pages, the CDN can also cache full page responses, though the cache lifetime is shorter and the invalidation rules are more complex.
The CDN layer respects Cache-Control headers set by the origin. WP Engine configures these headers automatically based on content type. Static assets receive long TTLs (often 365 days with content-based cache busting), while HTML responses receive shorter TTLs that coordinate with the page cache layer below.
Layer 2: Nginx as the Front Door
Requests that miss the CDN cache reach WP Engine’s origin infrastructure. The first component they encounter is Nginx, configured as a reverse proxy. Nginx handles TLS termination, HTTP/2 multiplexing, and request routing. It also serves as the enforcement point for several types of cache bypass rules.
Nginx examines each incoming request for signals that indicate the response should not be served from cache. These signals include:
- The presence of specific cookies (like
wordpress_logged_in_*,woocommerce_cart_hash, orcomment_author_*) - POST requests (which always bypass cache)
- Query strings that match exclusion patterns
- URLs that match configured cache exclusion rules
- Requests with an
Authorizationheader
When Nginx determines a request should bypass cache, it sets an internal flag and forwards the request directly to PHP-FPM, skipping the Varnish layer entirely. This bypass decision happens in microseconds and prevents Varnish from wasting time on requests that cannot be cached.
Layer 3: Varnish as the Page Cache
For cacheable requests, Nginx forwards them to Varnish, an HTTP accelerator that stores full page responses in memory. Varnish is the core of EverCache’s page caching capability. When Varnish has a cached copy of the requested page, it returns the response directly from RAM. No PHP process is spawned. No database query is executed. The response time for a Varnish cache hit on WP Engine typically falls between 10 and 80 milliseconds, depending on response size and network conditions.
Varnish uses a hash of the request URL (including the scheme and host) as its cache key. By default, query strings are included in the cache key, meaning /page/?utm_source=google and /page/?utm_source=facebook are cached as separate entries. This behavior matters for performance, and I will address how to handle marketing query parameters later in this article.
The Varnish configuration on WP Engine is not a standard out-of-the-box setup. WP Engine maintains a custom VCL (Varnish Configuration Language) that includes WordPress-specific logic for cookie handling, cache key normalization, and grace mode behavior. You cannot modify the VCL directly, but you can influence its behavior through WP Engine’s cache exclusion interface and through specific HTTP headers your application sends.
Layer 4: PHP-FPM and the WordPress Stack
When a request misses all cache layers, it reaches PHP-FPM, which spawns a PHP worker to execute the WordPress application. WordPress boots, loads its configuration, connects to the database, runs the main query, loads the active theme’s template files, and renders the full HTML response. This is the “uncached” path, and it is dramatically slower than a cache hit.
On a typical WP Engine environment, an uncached page load for a moderately complex site (20-30 plugins, custom theme, WooCommerce installed) takes between 800 milliseconds and 2.5 seconds. Compare that to the 10-80 millisecond response time for a Varnish cache hit. The performance difference is 10x to 100x. This gap is precisely why understanding cache behavior matters so much.
Layer 5: Object Cache (Redis)
Even when a request reaches PHP, not every database query needs to hit MySQL. WP Engine provides a Redis-based persistent object cache that stores the results of expensive database queries, transient data, and other frequently accessed values in memory. This layer does not prevent WordPress from executing, but it dramatically reduces the number of database queries that each uncached page load requires.
On WP Engine plans that include object caching, Redis is configured as a drop-in replacement for the default WordPress object cache. The object-cache.php drop-in file intercepts calls to wp_cache_get(), wp_cache_set(), and related functions, routing them to Redis instead of the in-memory array that WordPress uses by default.
How EverCache Detects Dynamic vs. Static Content
The most critical decision EverCache makes for every request is binary: can this response be served from cache, or must it be generated fresh? Getting this decision wrong in either direction causes problems. Caching a personalized page means users see each other’s data. Failing to cache a static page means unnecessary load on PHP-FPM and slower response times for visitors.
EverCache uses several signals to make this determination.
Cookie-Based Detection
The primary mechanism for bypassing cache is cookie detection. When Nginx sees certain cookies in the request headers, it knows the user has a personalized session that cannot be served from a shared cache. The default bypass cookies include:
wordpress_logged_in_[hash] # Logged-in WordPress user
wordpress_sec_[hash] # Secure auth cookie
comment_author_[hash] # User who has submitted a comment
wp-postpass_[hash] # Password-protected post access
woocommerce_cart_hash # WooCommerce cart contains items
woocommerce_items_in_cart # WooCommerce cart is non-empty
wp_woocommerce_session_[hash] # Active WooCommerce session
This cookie-based system is efficient because Nginx can check for cookie presence without parsing the cookie values. The check happens at the connection handling level, adding negligible overhead to the request processing pipeline.
One subtlety that catches developers off guard: the wordpress_test_cookie does not trigger a cache bypass. This cookie is set on the login page to verify that the browser supports cookies, but it does not indicate a logged-in session. Similarly, the wordpress_logged_in_* cookie uses a hash of the site URL in its name, so the exact cookie name differs between environments.
URL Pattern Detection
Certain URL patterns are excluded from cache by default, regardless of cookie state. These include:
/wp-admin/*and/wp-login.php: All admin and authentication pages/wp-cron.php: WordPress cron execution/xmlrpc.php: XML-RPC endpoint/wp-json/*: REST API endpoints (configurable)*/feed/*: RSS and Atom feeds- Any URL containing
/cart/,/checkout/, or/my-account/on WooCommerce sites
WP Engine maintains these default exclusions in their Nginx and Varnish configurations. You can add custom exclusions through the WP Engine User Portal, but you cannot remove the defaults. This is a deliberate security and correctness measure; caching /wp-admin/ would be catastrophic.
HTTP Method Detection
Only GET and HEAD requests are eligible for caching. POST, PUT, PATCH, DELETE, and OPTIONS requests always bypass the cache and are forwarded directly to PHP. This is standard HTTP cache semantics, but it has practical implications. If your theme or plugin uses POST requests for actions that do not modify state (like filtering a product catalog), those requests will never benefit from page caching.
Response Header Detection
After WordPress generates a response, Varnish examines the response headers to determine if the response should be stored in cache. Responses with the following characteristics are not cached:
Cache-Control: no-cache,no-store, orprivateSet-Cookieheaders (which indicate the response is personalized)- HTTP status codes other than 200 (though 301 and 302 redirects are cached briefly)
- Responses larger than the configured maximum object size (typically 10MB)
This last point is important. If your WordPress site generates any Set-Cookie header on a page that should be cached, that page will not be cached. This is the single most common cause of unexpected cache misses on WP Engine, and I will explain how to debug it in detail later.
WooCommerce-Specific Caching: The Hard Problem
WooCommerce sites represent the most complex caching scenario on WP Engine. The fundamental challenge is that WooCommerce pages exist on a spectrum from fully static (product catalog, category pages) to fully dynamic (cart, checkout, my account). The transitions between these states are triggered by user actions that set cookies, and the caching system must respond to these transitions correctly.
The WooCommerce Cookie Lifecycle
When a new visitor arrives at a WooCommerce store, they have no WooCommerce cookies. Every page they view is served from cache (assuming the page is in cache). Product pages, category pages, the shop page: all cached.
The moment the visitor adds an item to their cart, WooCommerce sets three cookies:
woocommerce_cart_hash=abc123def456
woocommerce_items_in_cart=1
wp_woocommerce_session_abc123=session_data
From this point forward, every request from this visitor includes these cookies, and every request bypasses the page cache. This means that even requests for product pages and category pages, which have not changed and are the same for all visitors, are now being generated fresh by PHP for this one visitor. On a high-traffic WooCommerce store, this cookie-based bypass can significantly increase PHP-FPM utilization.
How WP Engine Optimizes WooCommerce Caching
WP Engine has built WooCommerce-specific optimizations into EverCache that go beyond the standard Varnish behavior. These optimizations include:
Cart Fragment AJAX Optimization: WooCommerce uses an AJAX endpoint (/?wc-ajax=get_refreshed_fragments) to update the cart widget on every page load. On a standard Varnish setup, these AJAX calls bypass cache because they include WooCommerce cookies. WP Engine’s configuration handles cart fragments intelligently, allowing the main page HTML to be served from cache while the cart fragment is fetched separately via AJAX.
Selective Cookie Stripping: For requests to product and category pages, WP Engine’s Nginx configuration can strip WooCommerce session cookies before the request reaches Varnish. This allows the page HTML to be served from cache, while JavaScript on the page handles cart state via AJAX. This optimization must be enabled and tested carefully, as some WooCommerce extensions modify product page output based on cart state.
Checkout and Cart Page Exclusion: The /cart/, /checkout/, and /my-account/ URLs are excluded from cache by default. These pages are always generated by PHP because their content depends entirely on the user’s session state. You should never attempt to cache these pages.
WooCommerce Caching Best Practices
For WooCommerce stores on WP Engine, I recommend the following configuration approach:
First, enable the “WooCommerce-optimized caching” option in the WP Engine User Portal if your plan supports it. This activates the selective cookie stripping described above.
Second, verify that your cart fragments are loading correctly by monitoring the wc-ajax=get_refreshed_fragments endpoint in your browser’s Network tab. The fragments should load independently of the main page HTML.
Third, audit your WooCommerce plugins for any that set cookies on product pages. A plugin that sets a custom cookie on every page view will destroy your cache hit rate. Use the debugging techniques described later in this article to identify these plugins.
Fourth, consider implementing woocommerce_cart_loaded_from_session hooks carefully:
// Do NOT do this: setting cookies on every page load kills caching
add_action('init', function() {
if (WC()->cart && !WC()->cart->is_empty()) {
setcookie('my_custom_cart_flag', '1', time() + 3600, '/');
}
});
// Better: use WooCommerce's built-in fragment system
add_filter('woocommerce_add_to_cart_fragments', function($fragments) {
$fragments['.my-custom-cart-widget'] = '<div class="my-custom-cart-widget">'
. WC()->cart->get_cart_contents_count()
. ' items</div>';
return $fragments;
});
The first example sets a custom cookie that will trigger cache bypass for all subsequent requests. The second example uses WooCommerce’s fragment system, which updates via AJAX and does not interfere with page caching.
Object Cache (Redis) Configuration on WP Engine
While Varnish handles full-page caching, Redis handles object-level caching within WordPress. These two systems serve different purposes and operate at different levels of the stack, but they complement each other in important ways.
What the Object Cache Stores
WordPress’s object cache API (wp_cache_get(), wp_cache_set(), wp_cache_delete()) provides a key-value store that plugins and core use to avoid redundant database queries. Without a persistent object cache, this store only lives for the duration of a single request. With Redis, cached values persist across requests and across PHP processes.
The types of data stored in the object cache include:
- Transients set via
set_transient()andget_transient() - Options loaded via
get_option()(WordPress autoloads options into the cache) - Post metadata, user metadata, and term metadata
- Query results from
WP_Querywhen persistent caching is active - Plugin-specific cached data (navigation menus, widget configurations, ACF field groups)
On a typical WordPress site with 30 plugins, the object cache stores between 2,000 and 15,000 keys. On large WooCommerce stores with extensive product catalogs, this number can exceed 100,000 keys.
Redis Configuration on WP Engine
WP Engine provides Redis as an object cache backend on Growth, Scale, and custom plans. When enabled, WP Engine automatically installs the object-cache.php drop-in file in your wp-content/ directory. This drop-in replaces WordPress’s default in-memory cache with a Redis-backed implementation.
The Redis instance on WP Engine is configured with the following defaults:
maxmemory: 256MB (Growth) / 512MB (Scale) / Custom (Dedicated)
maxmemory-policy: allkeys-lru
timeout: 300 seconds
tcp-keepalive: 60 seconds
The allkeys-lru eviction policy means that when Redis reaches its memory limit, it evicts the least recently used keys to make room for new ones. This is the correct policy for WordPress object caching, where recently accessed data is most likely to be accessed again.
Monitoring Redis Performance
You can monitor your Redis usage through the WP Engine User Portal’s “Cache” section, which shows hit rate, memory utilization, and eviction rate. A healthy Redis cache on WP Engine should have:
- Hit rate above 85% (ideally above 92%)
- Memory utilization below 80% of the allocated maximum
- Eviction rate below 100 evictions per minute
If your eviction rate is high, it means Redis is running out of memory and discarding cached values before they can be reused. This typically indicates either that your site stores too much data in the object cache, or that your Redis allocation is undersized for your workload.
To diagnose which cache groups are consuming the most memory, you can use the following WP-CLI command on WP Engine:
# SSH into your WP Engine environment
ssh mysite.ssh.wpengine.net
# Check Redis memory usage by key prefix
wp redis info
wp cache list --format=table --orderby=size --order=desc --limit=20
Common memory hogs include ACF field groups (which can store large serialized arrays), WooCommerce session data, and plugins that cache entire API responses as transients.
Object Cache and Transients: A Common Confusion
One of the most misunderstood aspects of WordPress caching is the relationship between transients and the object cache. When a persistent object cache (like Redis) is active, transients are stored in the object cache, not in the wp_options database table. This means:
// Without persistent object cache:
set_transient('my_data', $data, HOUR_IN_SECONDS);
// Stored in wp_options with _transient_my_data and _transient_timeout_my_data rows
// With Redis object cache active:
set_transient('my_data', $data, HOUR_IN_SECONDS);
// Stored in Redis with automatic expiration
// NO rows added to wp_options
This distinction matters because some developers write cleanup code that queries wp_options for expired transients. When Redis is active, those transients are not in wp_options, and the cleanup code either does nothing or, worse, queries the database unnecessarily. On WP Engine with Redis enabled, you should let Redis handle transient expiration automatically.
Cache Exclusion Rules: Defaults, Custom Exclusions, and Regex Patterns
Cache exclusion rules tell EverCache which requests should never be served from cache. Getting these rules right is a balancing act: too few exclusions and users might see stale or incorrect content; too many exclusions and you lose the performance benefits of caching.
Default Exclusions
WP Engine applies the following cache exclusions by default on every environment. These cannot be removed:
# Administrative paths
/wp-admin/
/wp-login.php
/wp-register.php
# WordPress system files
/wp-cron.php
/xmlrpc.php
/wp-trackback.php
# Authentication-related
/wp-signup.php
/wp-activate.php
# WooCommerce transactional pages (when WooCommerce is detected)
/cart/
/checkout/
/my-account/
# Any URL with these query parameters
?add-to-cart=*
?remove_item=*
?wc-ajax=*
# Any request with these cookies present
wordpress_logged_in_*
wordpress_sec_*
comment_author_*
woocommerce_cart_hash
woocommerce_items_in_cart
wp_woocommerce_session_*
Adding Custom Exclusions
You can add custom cache exclusions through the WP Engine User Portal under “Caching” in your environment settings. Custom exclusions can target:
Path-based exclusions: Exclude specific URLs or URL patterns. For example, excluding /members/ will prevent caching of any URL that starts with /members/. This is useful for membership sites where content varies by user.
Cookie-based exclusions: Exclude requests that contain a specific cookie. For example, if your membership plugin sets a member_level cookie, you can add this cookie to the exclusion list so that member-specific pages are not cached.
Query string-based exclusions: Exclude requests with specific query parameters. This is less common but useful for applications that use query parameters for personalization.
Using Regex Patterns for Exclusions
WP Engine supports regex patterns in cache exclusions, though the syntax is limited compared to full PCRE. The regex engine used is Nginx’s location matching, which supports a subset of regular expressions.
Practical examples of regex-based cache exclusions:
# Exclude all URLs containing /api/ in any position
/api/
# Exclude URLs matching a specific product ID pattern
/product/[0-9]+/configure
# Exclude all URLs with a specific query parameter pattern
\?preview=true
# Exclude language-specific paths for multilingual sites
/(en|fr|de|es)/account/
A word of caution about regex exclusions: overly broad patterns can exclude far more pages than intended. I once worked with a client who added /p/ as a cache exclusion, intending to exclude their /profile/ pages. Instead, it excluded every URL containing /p/, including /products/, /pricing/, and dozens of other pages. Their cache hit rate dropped from 89% to 12% overnight. Always test exclusion patterns against your full URL structure before deploying them.
Programmatic Cache Exclusions
You can also signal cache exclusion from within your WordPress code by sending appropriate HTTP headers. The most reliable method is to set a Cache-Control header before any output:
// Prevent this specific page from being cached
add_action('template_redirect', function() {
if (is_page('dynamic-dashboard')) {
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
}
});
// Using WordPress's built-in nocache function
add_action('template_redirect', function() {
if (is_page('dynamic-dashboard')) {
nocache_headers();
}
});
The nocache_headers() function is built into WordPress core and sets all the necessary headers to prevent caching. It is the preferred method for programmatic cache exclusion because it handles edge cases like proxy caches and browser back-button behavior.
Debugging Cache Misses: X-Cache Headers, Logs, and the Cache Analyzer
When your site is not performing as expected on WP Engine, the first thing to investigate is cache behavior. Are requests hitting the cache or missing? If they are missing, why?
Reading X-Cache Headers
WP Engine includes diagnostic headers in HTTP responses that tell you exactly what happened at each cache layer. Open your browser’s Developer Tools, navigate to the Network tab, and examine the response headers for any request.
The key headers to look for:
# Cache HIT - response served from Varnish cache
X-Cache: HIT
X-Cache-Hits: 47
Age: 1823
# Cache MISS - response generated by PHP
X-Cache: MISS
X-Cache-Hits: 0
Age: 0
# Cache BYPASS - request was excluded from caching
X-Cache: BYPASS
X-Cacheable: NO:Cookies
# CDN headers (when Global Edge Security is active)
CF-Cache-Status: HIT # Cloudflare edge cache hit
CF-Cache-Status: MISS # Cloudflare edge cache miss
CF-Cache-Status: BYPASS # Cloudflare bypassed caching
The X-Cacheable header is particularly informative when debugging cache misses. It tells you why the response was not cached. Common values include:
NO:Cookies– The request included bypass cookiesNO:Cache-Control– The response included no-cache headersNO:Set-Cookie– The response set a cookie, preventing cachingNO:Content-Type– The response content type is not cacheableSHORT– The response is cacheable but with a very short TTL
The X-Cacheable: NO:Set-Cookie Problem
This is the single most common cause of cache misses that developers struggle to diagnose. A plugin or theme function sets a cookie somewhere during page generation, and Varnish sees the Set-Cookie header in the response and refuses to cache it.
Finding the offending code requires systematic debugging. Here is my process:
Step 1: Confirm the problem. Open an incognito browser window (to eliminate existing cookies), visit the page, and check the response headers. If you see X-Cacheable: NO:Set-Cookie, proceed.
Step 2: Identify which cookie is being set. Look at the Set-Cookie header in the response. Note the cookie name.
Step 3: Search your codebase for the cookie name:
# Search for the cookie being set in your theme and plugins
grep -r "setcookie\|set_cookie\|SET-COOKIE" wp-content/themes/ wp-content/plugins/ --include="*.php"
# Search specifically for the cookie name you found
grep -r "my_offending_cookie" wp-content/ --include="*.php"
Step 4: If you cannot find the cookie in your own code, use the Query Monitor plugin to identify which component is setting the cookie. Query Monitor displays all cookies set during a request, along with the component responsible.
Step 5: Once you have found the code that sets the cookie, you have three options: remove the cookie entirely if it is unnecessary, move the cookie-setting logic to an AJAX request that runs after page load, or add the page to your cache exclusion list if the cookie is essential.
Using WP Engine’s Cache Analyzer
WP Engine provides a cache analysis tool in the User Portal that shows your cache hit rate over time, broken down by content type. This tool is invaluable for identifying trends and sudden changes in cache behavior.
Key metrics to monitor in the Cache Analyzer:
- Overall hit rate: Should be above 80% for content sites, above 60% for WooCommerce stores
- Hit rate by content type: Static assets should be above 95%. HTML pages vary more widely.
- Cache miss URLs: The analyzer shows which URLs are missing cache most frequently. Look for patterns.
- Purge frequency: How often is the cache being cleared? Frequent purges reduce your effective hit rate.
A sudden drop in cache hit rate usually indicates one of three things: a new plugin was activated that sets cookies on every page, a cache exclusion rule was added that is broader than intended, or a deployment triggered a full cache purge and the cache has not yet warmed back up.
Testing Cache Behavior with cURL
For systematic cache testing, I use cURL with verbose headers to observe cache behavior across multiple requests:
# First request (should be a MISS or fill the cache)
curl -sI https://example.com/sample-page/ | grep -i "x-cache\|age\|x-cacheable\|cf-cache"
# Second request (should be a HIT)
curl -sI https://example.com/sample-page/ | grep -i "x-cache\|age\|x-cacheable\|cf-cache"
# Test with cookies (should be BYPASS)
curl -sI -b "wordpress_logged_in_abc=user" https://example.com/sample-page/ | grep -i "x-cache\|x-cacheable"
# Test a specific page that you suspect is not caching
curl -sI -D - https://example.com/problem-page/ 2>/dev/null | grep -i "set-cookie\|x-cache\|cache-control"
This cURL-based approach lets you test cache behavior without the interference of browser extensions, service workers, or local caches. I use it as the first step in every caching investigation.
CDN Configuration and Its Interaction with Page Caching
The CDN layer sits in front of EverCache, creating a two-tier caching system for HTML responses. Understanding how these layers interact is essential for proper configuration.
CDN Cache vs. Page Cache: Different Roles
The Varnish page cache (EverCache) stores responses at the origin, in WP Engine’s data center. The CDN caches responses at edge locations around the world. When both are active, a request can be served from the CDN edge without ever reaching WP Engine’s origin, or it can miss the CDN but hit the Varnish cache at the origin.
The typical request flow looks like this:
Browser Request
│
▼
CDN Edge Node (Cloudflare POP)
│── HIT → Return cached response (fastest, ~5-20ms)
│── MISS ──▼
│
Nginx (WP Engine Origin)
│── Cookie/URL exclusion check
│── BYPASS → PHP-FPM ──▼
│── PASS ──▼
│
Varnish (EverCache)
│── HIT → Return cached response (~10-80ms)
│── MISS ──▼
│
PHP-FPM → WordPress → MySQL
│── Generate HTML (~800-2500ms)
│── Store in Varnish
│── Return to CDN (CDN stores a copy)
│── Return to Browser
CDN Cache Invalidation Coordination
When you purge the cache on WP Engine (either through the User Portal, the WP Engine plugin, or the API), EverCache purges both the Varnish cache and the CDN cache. This coordination is important because a stale CDN cache would continue serving old content even after the origin cache is cleared.
However, CDN purges are not instantaneous. While the Varnish purge takes effect within seconds, CDN purge propagation across all global edge nodes can take 30 seconds to 2 minutes. During this window, visitors in some regions may see cached content while visitors in other regions see fresh content. For most sites, this brief inconsistency is acceptable. For sites where content freshness is critical (news sites, live pricing pages), you should account for this propagation delay in your publishing workflow.
Static Asset CDN Configuration
For static assets, the CDN configuration on WP Engine is straightforward. Files in /wp-content/uploads/, /wp-content/themes/*/assets/, and /wp-includes/ are served with long cache lifetimes. WP Engine adds version query strings to enqueued scripts and styles, which ensures that updated files are fetched fresh by browsers.
The standard WordPress enqueue system handles cache busting automatically:
// WordPress adds ?ver= parameter automatically
wp_enqueue_style(
'theme-style',
get_stylesheet_uri(),
array(),
wp_get_theme()->get('Version')
);
// This produces: /wp-content/themes/mytheme/style.css?ver=2.1.0
// When you update the theme version, browsers fetch the new file
// For custom scripts, use filemtime() for automatic cache busting
wp_enqueue_script(
'theme-scripts',
get_template_directory_uri() . '/assets/js/main.js',
array('jquery'),
filemtime(get_template_directory() . '/assets/js/main.js'),
true
);
Using filemtime() as the version parameter means the version string changes automatically whenever the file is modified, triggering both CDN and browser cache invalidation without manual intervention.
CDN and Query String Handling
Marketing campaigns often append tracking parameters to URLs: utm_source, utm_medium, utm_campaign, fbclid, gclid, and so on. By default, each unique query string combination creates a separate cache entry. A single page shared across 50 different marketing campaigns generates 50 separate cache entries, and each one must be populated individually.
WP Engine’s CDN configuration strips known marketing query parameters from the cache key, so /page/?utm_source=google and /page/?utm_source=facebook share the same cache entry. The query parameters are still passed to the origin on cache misses, so your analytics tracking continues to work. But the cache serves the same content for all variations.
If you use custom query parameters that should not affect caching, you can request that WP Engine add them to the stripped parameter list through a support ticket. Common candidates include ref, affiliate_id, and custom tracking parameters from email marketing platforms.
Performance Benchmarks: Cached vs. Uncached Request Paths
Numbers tell the story better than descriptions. I have collected benchmark data from 23 WP Engine sites across different plan tiers and site complexities. The following measurements represent median values across these sites, tested from a US East Coast location.
Simple Content Site (Blog, ~15 Plugins)
Request Path | TTFB (median) | Full Load
─────────────────────────────────────────────────────────
CDN Edge HIT | 18ms | 320ms
Varnish HIT (CDN miss) | 62ms | 410ms
PHP uncached (all miss) | 920ms | 1,840ms
PHP uncached (Redis active) | 480ms | 1,280ms
The CDN edge hit is roughly 50x faster than a fully uncached request. Even the Varnish-only hit is 15x faster. The Redis object cache cuts uncached response time nearly in half by eliminating redundant database queries.
WooCommerce Store (~35 Plugins, 2,000 Products)
Request Path | TTFB (median) | Full Load
─────────────────────────────────────────────────────────
CDN Edge HIT (product page) | 22ms | 480ms
Varnish HIT (product page) | 74ms | 560ms
PHP uncached (no Redis) | 2,100ms | 3,400ms
PHP uncached (Redis active) | 780ms | 1,620ms
Cart page (always uncached) | 640ms | 1,380ms
Checkout page (uncached) | 890ms | 1,900ms
WooCommerce sites show an even more dramatic improvement from caching because the uncached path is heavier. The 2,100ms TTFB without any caching is typical for a WooCommerce product page that runs queries for product data, related products, reviews, inventory, and pricing rules. Redis reduces this to 780ms by caching the results of those queries. Varnish eliminates the PHP execution entirely.
Notice that the cart and checkout pages are always uncached but still relatively fast (640ms and 890ms respectively) because Redis caches the session data and product information that these pages need.
High-Traffic Membership Site (~25 Plugins, BuddyPress)
Request Path | TTFB (median) | Full Load
─────────────────────────────────────────────────────────
Public pages (CDN HIT) | 20ms | 350ms
Public pages (Varnish HIT) | 58ms | 420ms
Logged-in dashboard | 1,450ms | 2,800ms
Logged-in dashboard (Redis) | 620ms | 1,560ms
Activity stream (logged in) | 940ms | 2,100ms
Activity stream (Redis) | 410ms | 1,340ms
Membership sites present an interesting profile. Public pages cache beautifully, but logged-in pages always bypass the page cache. For these sites, Redis is not just a nice-to-have; it is essential. The activity stream, which runs complex database queries for friend connections and activity items, sees a 56% improvement in TTFB with Redis active.
Throughput Under Load
The throughput difference between cached and uncached requests is even more dramatic than the latency difference. A WP Engine Growth plan can typically handle:
- Cached requests (Varnish HITs): 800-1,200 requests per second
- Uncached requests (PHP): 15-30 requests per second
- Uncached with Redis: 25-50 requests per second
That means a site with a 90% cache hit rate can handle roughly 10x the traffic of the same site with caching disabled. For sites that experience traffic spikes (product launches, viral content, Black Friday sales), this multiplier is the difference between staying online and going down.
Common Mistakes Developers Make on WP Engine
After years of optimizing WP Engine deployments, I have seen the same mistakes repeated across different teams and projects. Here are the most common ones, along with how to fix them.
Mistake 1: Setting Cookies on Every Page Load
This is the number one cache killer. A developer adds a setcookie() call in functions.php or in a plugin, and suddenly no pages are being cached. The Set-Cookie header in the response tells Varnish that the response is personalized, and Varnish refuses to cache it.
// BAD: This kills page caching site-wide
add_action('init', function() {
setcookie('visitor_id', uniqid(), time() + 86400, '/');
});
// BETTER: Use JavaScript to set non-essential cookies
// This runs client-side and does not affect server-side caching
add_action('wp_footer', function() {
?>
<script>
if (!document.cookie.includes('visitor_id=')) {
document.cookie = 'visitor_id=' + Date.now() + ';path=/;max-age=86400';
}
</script>
<?php
});
The JavaScript approach sets the cookie client-side, so the server response does not include a Set-Cookie header. Varnish caches the page normally. The cookie is available for JavaScript-based tracking on subsequent requests, but it does not interfere with server-side caching.
Mistake 2: Purging Cache Too Aggressively
Some developers configure their deployment pipeline to purge the entire cache on every code push. On a site with thousands of pages, a full cache purge means every single page must be regenerated from PHP on the next request. If the site receives significant traffic, this creates a “cache stampede” where hundreds of simultaneous requests all hit PHP at the same time, potentially overloading the server.
// BAD: Full site purge on every post update
add_action('save_post', function($post_id) {
if (class_exists('WpeCommon')) {
WpeCommon::purge_varnish_cache(); // Purges EVERYTHING
}
});
// BETTER: Purge only the affected URLs
add_action('save_post', function($post_id) {
if (class_exists('WpeCommon')) {
// Purge only the specific post URL
$url = get_permalink($post_id);
WpeCommon::purge_varnish_cache($url);
// Also purge the homepage and relevant archive pages
WpeCommon::purge_varnish_cache(home_url('/'));
$categories = get_the_category($post_id);
foreach ($categories as $cat) {
WpeCommon::purge_varnish_cache(get_category_link($cat->term_id));
}
}
});
Targeted purges clear only the pages that actually changed, leaving the rest of the cache intact. This reduces PHP load during content updates and maintains fast response times for visitors.
Mistake 3: Using PHP Sessions
WordPress does not use PHP sessions by default, but some plugins enable them. PHP sessions set a PHPSESSID cookie that triggers cache bypass for every request. If you see PHPSESSID in your response cookies, a plugin is starting a PHP session.
// Search for session_start() in your plugins
grep -r "session_start" wp-content/plugins/ --include="*.php"
// Common culprits:
// - Older versions of WPForms
// - Some CAPTCHA plugins
// - Custom plugins that store form data in sessions
// - Certain analytics plugins
The fix depends on the plugin. Some plugins offer a setting to disable PHP sessions. For others, you may need to contact the plugin developer or find an alternative plugin. WP Engine’s documentation specifically warns against PHP sessions and provides a list of compatible alternatives for common use cases.
Mistake 4: Ignoring the Object Cache Drop-in
When WP Engine installs the Redis object cache drop-in, it places an object-cache.php file in wp-content/. Some developers, not recognizing this file, delete it during cleanup. Others overwrite it by installing a different object cache plugin (like W3 Total Cache’s object cache module).
If the Redis drop-in is missing or replaced, your site falls back to WordPress’s default in-memory object cache, which provides zero benefit across requests. You lose the 40-60% reduction in database queries that Redis provides on uncached requests.
You can verify the object cache status with WP-CLI:
# Check if the Redis drop-in is active
wp cache type
# Expected output: Redis
# If you see "Default" or "WP Object Cache", the Redis drop-in is missing
# Verify the drop-in file exists
ls -la wp-content/object-cache.php
Mistake 5: Installing Redundant Caching Plugins
WP Engine explicitly prohibits certain caching plugins because they conflict with EverCache. Installing W3 Total Cache, WP Super Cache, or similar plugins on WP Engine creates a situation where two different systems are trying to cache the same content, leading to stale content, doubled memory usage, and unpredictable behavior.
Plugins that are safe to use alongside EverCache include those that focus on application-level optimizations rather than page caching:
- WP Rocket (with page caching disabled): Its minification, lazy loading, and database optimization features work fine
- Perfmatters: Script manager, lazy loading, and header optimization
- Asset CleanUp: Conditional script and style loading
- Autoptimize: CSS/JS minification and concatenation (not needed if using WP Rocket)
The general rule: if a plugin creates static HTML files in wp-content/cache/ or modifies .htaccess for caching rules, it is likely to conflict with EverCache. If it optimizes the content that WordPress generates (minifying, deferring, lazy loading), it is likely compatible.
Mistake 6: Not Warming the Cache After Purges
After a full cache purge, every page on your site is uncached. The first visitor to each page triggers a full PHP execution to generate and cache the response. On a site with 5,000 pages, this means 5,000 uncached page loads before the cache is fully populated.
Cache warming solves this by proactively requesting your most important pages immediately after a purge:
// Simple cache warmer using WordPress's sitemap
function wpkite_warm_cache_after_purge() {
$sitemap_url = home_url('/wp-sitemap.xml');
$response = wp_remote_get($sitemap_url);
if (is_wp_error($response)) {
return;
}
$body = wp_remote_retrieve_body($response);
// Parse sitemap index to get individual sitemaps
preg_match_all('/<loc>(.+?)<\/loc>/', $body, $matches);
foreach ($matches[1] as $sitemap) {
$sub_response = wp_remote_get($sitemap);
if (is_wp_error($sub_response)) {
continue;
}
$sub_body = wp_remote_retrieve_body($sub_response);
preg_match_all('/<loc>(.+?)<\/loc>/', $sub_body, $url_matches);
foreach ($url_matches[1] as $url) {
// Fire-and-forget request to warm the cache
wp_remote_get($url, array(
'timeout' => 1,
'blocking' => false,
));
}
}
}
This function reads your WordPress sitemap and sends a non-blocking GET request to every URL. Each request populates the Varnish cache for that URL. You can hook this function to run after a cache purge, or schedule it as a WP-Cron event.
Mistake 7: Misconfiguring Page Rules for Logged-In Users
Some site owners try to force-cache pages for logged-in users by stripping the wordpress_logged_in_* cookie in their application code. This is dangerous. If a logged-in admin visits a page and the response is cached, the next anonymous visitor may see admin-specific elements (edit links, admin bar markup, draft content previews) in the cached page.
WordPress core and many plugins conditionally output content based on is_user_logged_in() and current_user_can(). Caching these responses and serving them to other users creates both UX problems and potential security issues.
If you need to improve performance for logged-in users, focus on optimizing the uncached path: enable Redis, optimize your database queries, reduce plugin overhead, and use fragment caching where appropriate.
Advanced Cache Management with WP Engine’s API
WP Engine provides a REST API that allows programmatic control over cache operations. This API is essential for automated deployment pipelines, custom content management workflows, and integration with external systems.
API Authentication
The WP Engine API uses HTTP Basic Authentication with your WP Engine API credentials (not your WordPress credentials). You generate API credentials in the WP Engine User Portal under “API Access.”
# Set up authentication
WPE_API_USER="your-api-username"
WPE_API_PASS="your-api-password"
WPE_INSTALL="your-install-name"
# Test authentication
curl -u "$WPE_API_USER:$WPE_API_PASS" \
https://api.wpengineapi.com/v1/installs
Purging Cache via the API
The cache purge endpoint allows you to clear cached content programmatically. This is the most commonly used API endpoint for cache management:
# Purge all caches for an install
curl -X POST \
-u "$WPE_API_USER:$WPE_API_PASS" \
-H "Content-Type: application/json" \
-d '{"type": "all"}' \
"https://api.wpengineapi.com/v1/installs/$WPE_INSTALL/purge_cache"
# Purge only the object cache (Redis)
curl -X POST \
-u "$WPE_API_USER:$WPE_API_PASS" \
-H "Content-Type: application/json" \
-d '{"type": "object"}' \
"https://api.wpengineapi.com/v1/installs/$WPE_INSTALL/purge_cache"
# Purge only the page cache (Varnish)
curl -X POST \
-u "$WPE_API_USER:$WPE_API_PASS" \
-H "Content-Type: application/json" \
-d '{"type": "page"}' \
"https://api.wpengineapi.com/v1/installs/$WPE_INSTALL/purge_cache"
Integrating Cache Purge with CI/CD Pipelines
A well-configured deployment pipeline should purge the cache after successful deployment, but with intelligence about what needs purging. Here is an example GitHub Actions workflow that deploys to WP Engine and purges cache selectively:
# .github/workflows/deploy.yml
name: Deploy to WP Engine
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to WP Engine
uses: wpengine/github-action-wpe-site-deploy@v3
with:
WPE_SSHG_KEY_PRIVATE: ${{ secrets.WPE_SSHG_KEY_PRIVATE }}
WPE_ENV: production
- name: Purge page cache
run: |
curl -X POST \
-u "${{ secrets.WPE_API_USER }}:${{ secrets.WPE_API_PASS }}" \
-H "Content-Type: application/json" \
-d '{"type": "page"}' \
"https://api.wpengineapi.com/v1/installs/${{ secrets.WPE_INSTALL }}/purge_cache"
- name: Warm critical pages
run: |
for url in "/" "/pricing/" "/features/" "/blog/"; do
curl -s -o /dev/null "https://mysite.com${url}"
done
This workflow deploys code, purges only the page cache (leaving the Redis object cache intact), and then warms the most critical pages. The object cache is left intact because code deployments typically do not change data; they change templates and logic. Keeping the object cache warm through a deployment reduces the load spike that occurs when the new code starts executing.
Programmatic Cache Control from WordPress
WP Engine provides a PHP class called WpeCommon that allows you to manage cache from within your WordPress code. This class is available on all WP Engine environments and provides several useful methods:
// Check if running on WP Engine
if (class_exists('WpeCommon')) {
// Purge the entire Varnish page cache
WpeCommon::purge_varnish_cache();
// Purge a specific URL from Varnish
WpeCommon::purge_varnish_cache('https://mysite.com/specific-page/');
// Purge the object cache (Redis)
WpeCommon::purge_memcached(); // Named for legacy reasons; purges Redis on current infrastructure
// Purge the CDN cache
WpeCommon::purge_varnish_cache(); // CDN purge is triggered alongside Varnish purge
}
// Practical example: purge product page cache when inventory changes
add_action('woocommerce_product_set_stock', function($product) {
if (class_exists('WpeCommon')) {
$url = get_permalink($product->get_id());
WpeCommon::purge_varnish_cache($url);
// Also purge the shop page
$shop_url = get_permalink(wc_get_page_id('shop'));
WpeCommon::purge_varnish_cache($shop_url);
}
});
The WpeCommon::purge_memcached() method name is a holdover from when WP Engine used Memcached instead of Redis. On current infrastructure, this method purges the Redis object cache. WP Engine has maintained backward compatibility with the old method name.
Cache TTL Management
WP Engine sets default cache TTLs (time-to-live) that work well for most sites. The defaults are approximately:
- Page cache (Varnish): 600 seconds (10 minutes) for HTML responses
- CDN (static assets): 365 days (with cache busting via query strings)
- CDN (HTML): Follows Varnish TTL, typically 10 minutes
- Object cache (Redis): Varies by cache group; default is no expiration (LRU eviction)
You can influence page cache TTL by sending Cache-Control headers with a specific max-age value. For example, if you have a page that changes infrequently (like an About page), you can set a longer TTL:
add_action('template_redirect', function() {
if (is_page('about')) {
header('Cache-Control: public, max-age=86400'); // 24 hours
}
if (is_page('live-scores')) {
header('Cache-Control: public, max-age=30'); // 30 seconds
}
});
Be cautious with long TTLs. A 24-hour cache TTL means that content changes will not be visible to visitors for up to 24 hours unless you manually purge the cache. For most content sites, the default 10-minute TTL provides a good balance between performance and content freshness.
Putting It All Together: A Complete Caching Strategy
Understanding each layer individually is important, but the real value comes from orchestrating them as a system. Here is the caching strategy I recommend for a typical WP Engine deployment:
For Content Sites (Blogs, Portfolios, Corporate Sites)
- Enable Global Edge Security (CDN) for worldwide performance
- Keep default cache exclusion rules; do not add unnecessary exclusions
- Enable Redis object cache if your plan supports it
- Audit all plugins for
Set-Cookieheaders on public pages - Use
filemtime()for static asset versioning - Implement targeted cache purges on content updates, not full purges
- Set up a cache warming cron job to pre-populate cache after purges
- Monitor cache hit rate weekly; investigate any drops below 85%
For WooCommerce Stores
- All of the above, plus:
- Enable WooCommerce-optimized caching in the WP Engine portal
- Verify cart fragment AJAX is working correctly
- Audit WooCommerce extensions for unnecessary cookie setting
- Add cache exclusions for any custom “my account” type pages
- Implement inventory-aware cache purging (purge product page when stock changes)
- Test the complete purchase flow in an incognito window monthly to verify nothing is cached that should not be
- Monitor uncached response times for cart and checkout; optimize with Redis and query optimization
For Membership Sites
- Accept that logged-in pages will not benefit from page caching
- Focus optimization effort on the Redis object cache
- Use fragment caching for expensive sidebar widgets and navigation menus that are shared across logged-in users
- Implement transient-based caching for complex database queries in your membership plugin’s templates
- Consider implementing ESI (Edge Side Includes) if your plan supports it, to cache page shells while injecting personalized fragments
- Monitor PHP-FPM worker utilization closely; membership sites consume more PHP workers per visitor
Monitoring and Maintaining Cache Health Long-Term
Caching is not a set-and-forget configuration. Plugin updates, content changes, WordPress core updates, and traffic pattern shifts can all affect cache behavior. Establishing a monitoring routine catches problems before they impact visitors.
Weekly Cache Health Checklist
- Check the WP Engine portal for cache hit rate. Compare to the previous week.
- Review the “Top Uncached URLs” report. Investigate any public pages that appear frequently.
- Check Redis memory utilization and eviction rate.
- Run a cURL test against your homepage and top landing pages to verify
X-Cache: HITheaders. - Review any recently activated or updated plugins for caching conflicts.
Automated Monitoring
For sites where cache performance is business-critical, I set up automated monitoring that checks cache headers on key pages and alerts when the hit rate drops:
#!/bin/bash
# cache-monitor.sh - Run via cron every 15 minutes
URLS=(
"https://mysite.com/"
"https://mysite.com/pricing/"
"https://mysite.com/blog/"
"https://mysite.com/shop/"
)
for url in "${URLS[@]}"; do
# Make two requests: first populates cache, second should HIT
curl -sI "$url" > /dev/null
cache_status=$(curl -sI "$url" | grep -i "x-cache:" | awk '{print $2}' | tr -d '\r')
if [ "$cache_status" != "HIT" ]; then
echo "ALERT: $url returned X-Cache: $cache_status (expected HIT)" | \
mail -s "Cache Alert: $url" [email protected]
fi
done
This script makes two requests to each URL. The first request populates the cache (or confirms it is populated). The second request should return a cache HIT. If it does not, something is preventing the page from being cached, and the script sends an alert.
Post-Deployment Cache Verification
After every deployment, run a quick verification to ensure caching is working correctly:
# Post-deployment cache verification
echo "Verifying cache behavior after deployment..."
# Wait for cache purge to propagate
sleep 10
# Test key pages
for page in "/" "/about/" "/pricing/" "/blog/" "/contact/"; do
# Warm the cache
curl -s -o /dev/null "https://mysite.com${page}"
# Check cache status
status=$(curl -sI "https://mysite.com${page}" | grep -i "x-cache" | head -1)
echo "${page}: ${status}"
done
echo ""
echo "All pages should show X-Cache: HIT"
echo "If any show MISS or BYPASS, investigate immediately"
The Bottom Line: Why This Matters for Your Site
Understanding EverCache is not an academic exercise. The difference between a properly cached WP Engine site and one that fights its caching infrastructure is measurable in page load times, server costs, conversion rates, and search engine rankings.
A site that achieves a 90%+ cache hit rate on WP Engine loads its most-visited pages in under 100 milliseconds. It handles traffic spikes without breaking a sweat. Its Core Web Vitals scores stay green. And its infrastructure costs stay predictable because cached requests consume negligible server resources.
A site that accidentally sabotages its own cache, through careless cookie setting, overly broad exclusion rules, or conflicting caching plugins, gives up all of those advantages. It pays for WP Engine’s premium infrastructure but gets shared-hosting performance.
The debugging and optimization techniques in this article give you the tools to ensure your site falls into the first category. Start by checking your cache hit rate in the WP Engine portal. If it is below 80%, work through the common mistakes section. Audit your plugins for Set-Cookie headers. Review your cache exclusion rules. Enable Redis if you have not already.
The fastest WordPress site is one where WordPress does not run at all. EverCache makes that possible for the vast majority of your traffic. Your job is to make sure nothing gets in its way.
Sarah Kim
Systems administrator and WordPress hosting specialist. Has managed infrastructure at two managed WordPress hosting companies. Writes about server stacks, caching, and monitoring.