Why your WordPress cache is not working

A caching plugin is installed, the dashboard says everything is green, and the site still feels slow or the hit rate sits at zero. Nine times out of ten the plugin is working correctly and the cache is being bypassed for a reason you can identify in ten minutes. This article walks through the causes in order of how common they actually are on WooCommerce and content sites, how to verify which one is hitting you, and what to fix in each case.

A caching plugin that is installed and enabled does not mean pages are being served from cache. It means the plugin is capable of serving from cache. Whether it actually does depends on whether the request matches one of the bypass rules every serious cache plugin ships with, and on a WooCommerce site the most common answer is "yes, almost every request matches a bypass rule, and none of them are obvious". This article is the diagnostic path: how to tell whether a page is cached at all, and if not, why not.

For the underlying model of what each cache layer does, the caching-layers concept article is the background reading. This article assumes you already have a page cache plugin installed and want to know why it is not helping.

Is the page actually being cached

Before diagnosing a cause, confirm there is a miss to diagnose. Open an anonymous browser window (not an incognito tab of your logged-in profile, a separate browser, or curl). Request a known cacheable page twice. The second request should be a cache hit.

The check is in the response headers. Open your browser's DevTools (F12 or right-click, Inspect), go to the Network tab, and reload the page. Click the page request (the first HTML document in the list) and look at the Response Headers for a header called x-cache, cf-cache-status, age, x-proxy-cache, or x-nginx-cache. WP Rocket, LSCache, Varnish, nginx fastcgi_cache, and Cloudflare all name the header slightly differently, but the value is the same: HIT means the page was served from cache, MISS means it was not.

Reload the page a second time and check again. The second request should show HIT.

If you have SSH access, the same check works from the command line:

curl -sI https://yoursite.nl/sample-page/ | grep -Ei '^(x-cache|cf-cache-status|age|x-proxy-cache|x-nginx-cache)'

Expected output on a cache hit:

x-cache: HIT
age: 42

If you never see HIT on any request, the plugin is installed but nothing is being served from it. That is the scenario this article addresses. If the first request misses and the second hits, the plugin is working and your actual problem is either cache lifetime (pages expiring too fast) or a specific set of URLs that always miss.

The most common cause on WooCommerce sites: session cookies leaking onto every page

This is the single most common reason a WooCommerce site has a site-wide cache hit rate close to zero, and it is the one nobody notices because the caching plugin's own dashboard shows no errors.

The mechanism. Every serious caching layer bypasses the page cache when it sees a WooCommerce session cookie in the request. That is correct behavior for the cart and the checkout, where the cookie means "this visitor has a personal cart". The problem is that WooCommerce sets those cookies on every page where woocommerce.php is loaded, not only on cart and checkout. A visitor opens the homepage, wp_woocommerce_session_<hash> is set, they navigate to a blog post, the caching layer sees the cookie on that request, and the blog post is served uncached.

This is specifically covered in WooCommerce's own caching plugin documentation: the cookies woocommerce_cart_hash, woocommerce_items_in_cart, and wp_woocommerce_session_* are the ones that must trigger a bypass on dynamic pages. The WooCommerce docs do not explicitly warn that those cookies leak onto non-dynamic pages, but in practice they do, and that is what kills the hit rate.

How to verify this is your problem.

  1. Open your site in a fresh anonymous browser. Do not add anything to the cart.
  2. Visit the homepage, then a product page, then a blog post.
  3. In DevTools, open Application, then Cookies, and check for wp_woocommerce_session_<hash>. If it is there, the cookie is being set on a page that has nothing to do with the cart.
  4. Now request a normally cacheable page (a blog post, an About page) and check the cache headers as above. If that page was a HIT before you triggered the session and is now a MISS, the cookie is the cause.

The fix depends on your cache plugin.

  • WP Rocket and LiteSpeed Cache handle this correctly by default: their bypass rules trigger only on cart, checkout, and my-account URLs, not on the presence of the cookie itself. If you are on one of these plugins and still see the problem, the cause is usually a custom rule someone added to "bypass cache on WooCommerce cookies", which over-matches. Remove the custom rule.
  • WP Super Cache bypasses cache when any known bad cookie is present, including the WooCommerce session cookie. The fix is to edit wp-config.php or the wp-cache-config.php file and make sure wp_woocommerce_session_ is not in the $wp_cache_rejected_cookies list if you want those pages cached, or to use server-level bypass rules (nginx or Varnish) that scope the bypass to cart and checkout URLs only.
  • W3 Total Cache has a "Rejected Cookies" field under Page Cache, Advanced. The same rule applies: removing wp_woocommerce_session_ from that list allows the page cache to serve pages even when the cookie is present, and the cart and checkout still bypass correctly because WooCommerce sets the DONOTCACHEPAGE constant on those pages directly.
  • Server-level page cache (nginx fastcgi_cache, Varnish): the bypass logic is in the server configuration, not the plugin. Review the fastcgi_cache_bypass directive or the Varnish VCL and make sure the cookie-based bypass is scoped to cart and checkout paths, not global.

You will know it worked when the cache hit rate recovers on blog posts and non-cart pages while the cart, checkout, and my-account pages continue to show X-Cache: MISS (which is correct for those three pages).

Recent relevant change. WooCommerce 10.3 introduced an experimental feature that clears empty session cookies from clients, so that a visitor who has not actually added anything to the cart no longer carries the wp_woocommerce_session_* cookie around. Because it was shipped as experimental, it must be enabled explicitly and behavior may change in subsequent releases. On stores running 10.3 with the feature enabled, the cookie leakage problem is meaningfully reduced for browse-only traffic.

Logged-in users are not cached, and that is not a bug

This is the second most common complaint and the easiest to resolve, because the correct answer is "the cache is working and you are testing it wrong".

The mechanism. Every major page cache plugin (WP Rocket, WP Super Cache, W3TC, LiteSpeed Cache) excludes logged-in users from the page cache by default. A logged-in user receives a personalized admin bar, user-specific nonces, and role-specific content. Serving a shared cached page to a logged-in user would leak other users' state into their browser, so the plugin steps out of the way and serves a freshly built page every time.

WP Rocket documents this directly: "Pages as a logged-in user shouldn't be cached, unless you enable User Cache, log out of WordPress or visit the site using an incognito window" (source).

How to verify this is your problem. Log out of WordPress. Open an anonymous browser window (not your logged-in profile). Request the same page twice and check the response headers. If the second request is a HIT while logged out and a MISS while logged in, the plugin is working correctly.

The fix. Do not fix it. The bypass is correct. If logged-in performance is genuinely a problem (editors complaining that wp-admin is slow, members of a membership site reporting slow browsing), the right answer is not page caching. It is:

  1. A persistent object cache (Redis or Memcached) so that the database queries behind the page build are fast. The concept article explains what object caching does and how it differs from page caching.
  2. Diagnosing what is actually slow on the admin side. The slow WordPress admin article covers Heartbeat, autoload bloat, and the plugin admin gate, which are usually the real causes.
  3. WP Rocket's User Cache feature, which creates per-user cache files. This only makes sense on sites with very few logged-in users. On a membership site with ten thousand members it produces ten thousand cache files and is not an improvement over just running Redis.

You will know which problem you actually have when you test in an anonymous browser and see a HIT. If that works, the page cache is functional and you are looking at a separate question ("logged-in users feel slow") that page caching was never going to solve.

UTM parameters and query string fragmentation

This one is plugin-dependent, and the brief summary on every plugin's homepage hides real behavioral differences. The result is predictable: during an email or ad campaign, the cache hit rate drops to near zero because every click carries a unique query string and each unique URL gets its own cache entry.

The mechanism. A URL with a query string is, by default, a different URL for caching purposes. yoursite.nl/product/blue-shirt/ and yoursite.nl/product/blue-shirt/?utm_source=email are treated as two separate pages. If the cache stores them both, the first visitor from the email gets a miss (cache builds the page), and every subsequent visitor from the same email blast gets a hit against the UTM-suffixed variant. If the cache does not store query-string variants at all, every email click is a miss.

The critical detail: plugins handle this differently, and the default behavior is not what most people assume.

  • WP Rocket automatically strips known tracking parameters. Its documentation lists the supported parameters (utm_source, utm_medium, utm_campaign, utm_term, utm_content, gclid, fbclid, and several platform-specific codes). A URL with only these parameters gets the same cached file as the base URL.
  • WP Rocket edge case. If a URL contains a mix of ignored and non-ignored parameters, WP Rocket does not serve from cache. ?utm_source=email&country=fr misses because country is not in the ignore list. This is a documented behavior, not a bug, and it is a common cause of low hit rates on sites that combine campaign tracking with geolocation or personalization parameters. Same documentation source.
  • WP Super Cache and W3 Total Cache do not automatically ignore UTM parameters. By default, ?utm_source=email produces a separate cache entry. On a site using one of these plugins for the first time during a campaign, the hit rate can collapse to near zero for the duration.
  • LiteSpeed Cache strips UTM parameters by default in its "Drop Query String" setting, but this must be verified in the plugin's Cache, Excludes tab.
  • WordPress VIP platform-level cache does not filter UTM parameters either. VIP's own docs list what is automatically filtered, and UTM is not on that list.

How to verify this is your problem. Open a fresh anonymous browser. Request yoursite.nl/sample-page/ twice and confirm a second-request cache hit. Now request yoursite.nl/sample-page/?utm_source=test&utm_medium=email and check the cache header. If the hit rate drops on the UTM-suffixed URL, your plugin is not stripping query parameters.

The fix.

  • On WP Super Cache: Under Advanced, add utm_source, utm_medium, utm_campaign, utm_term, utm_content, gclid, fbclid to "Accepted Filenames & Rejected URIs", or better, use the plugin's own "Extra" tab to configure query string handling. WP Super Cache's documented approach is to explicitly allow certain parameters to be cached-equivalent.
  • On W3 Total Cache: Under Page Cache, Advanced, the "Accepted query strings" field lets you specify parameters that should be stripped from the cache key. Add the UTM parameters there.
  • On WP Rocket: It already handles the common cases. If you see misses, check whether a non-standard parameter is being added by your analytics setup. Add it to the ignored list if appropriate.
  • On server-level caches (Varnish, nginx): Strip UTM parameters in the cache key generation. A typical nginx snippet uses map to sanitize $args before building the key.

You will know it worked when a URL with only UTM parameters returns the same cache hit as the base URL on the second request.

Cart, checkout, and my-account must be excluded, and only those three

The flip side of the cookie problem is the overcorrection. A hosting support agent or a plugin wizard tells the site owner "WooCommerce is not cache-friendly, disable caching on the entire shop", and the owner disables it sitewide or excludes every page that contains the word "shop". This is the wrong fix.

The mechanism. WooCommerce itself defines exactly three pages that cannot be cached: cart, checkout, and my-account. WooCommerce's caching documentation is explicit about this: those three pages "need to stay dynamic since they display information specific to the current customer and their cart." Every other WooCommerce page (the shop archive, product pages, category pages, tag pages) is cacheable and should be cached. Product pages in particular are where most of the traffic sits on a successful store, and leaving them uncached for a mistaken safety margin is expensive.

WooCommerce sets the DONOTCACHEPAGE constant on cart, checkout, and my-account pages automatically. The constant is supported by WP Super Cache, W3 Total Cache, and WP Rocket, which means that on a correctly configured site those three pages bypass the cache without needing any manual rules at all. Since at least WooCommerce 1.4.2 the constant has been set automatically on the three dynamic pages.

How to verify this is your problem. In the cache plugin's exclusion list, look for entries beyond /cart, /checkout, and /my-account. Common over-excludes:

  • *shop* excluding the shop archive, which is cacheable
  • /product/* excluding all product pages, which are cacheable
  • *woocommerce* excluding anything with the word in the URL
  • /category/* excluding category pages on sites where the WooCommerce category prefix overlaps with a blog category prefix

The fix. Remove every exclusion that is not cart, checkout, or my-account. Product pages, category pages, and shop archives should be in the cache. Verify by requesting a product page twice anonymously; the second should be a HIT. If you are using an additional front-end endpoint (like a custom endpoint for deliveries or a builder preview), exclude only that specific endpoint, not the shop. Also exclude the WooCommerce AJAX endpoint /?wc-ajax=*, which was introduced in WooCommerce 2.4 in July 2015 as a replacement for admin-ajax.php, and which is used by cart fragments and several frontend interactions. It is not a page to cache.

You will know it worked when product and category pages return X-Cache: HIT on the second anonymous request while cart, checkout, and my-account still return MISS.

Cart fragments firing on every page

This one is older and less common on modern WooCommerce, but still worth ruling out on stores that have not updated recently.

The mechanism. WooCommerce's cart fragments script fires an AJAX call to ?wc-ajax=get_refreshed_fragments to keep the cart widget in sync with server-side state. Before WooCommerce 7.8 in 2023, that script was enqueued on every front-end page by default, which meant every page view triggered an uncached PHP request. The page itself could still be cached, but the AJAX call landed on PHP and ran through the full WooCommerce stack on every page load.

WooCommerce 7.8 changed the default so the cart fragments script only loads when (a) a cart widget is actually present on the page, (b) a third-party script declares it as a dependency, or (c) it is explicitly enqueued. Same source. On modern WooCommerce with default themes, the extra PHP hit is gone from pages that do not show a cart widget.

How to verify this is your problem. Open DevTools, Network tab, and filter by wc-ajax. Load a blog post or any non-shop page. If you see a get_refreshed_fragments request, the cart fragments script is still firing on every page.

The fix. On WooCommerce 7.8 or newer, remove the cart widget from non-shop pages, or verify a theme or plugin is not re-enqueuing the script. On older WooCommerce or in themes that have been patched to keep the fragments firing, add a snippet in your theme's functions.php to dequeue it:

// Dequeue cart fragments script outside shop, cart, and checkout pages.
add_action( 'wp_enqueue_scripts', function () {
    if ( is_shop() || is_cart() || is_checkout() || is_product() ) {
        return;
    }
    wp_dequeue_script( 'wc-cart-fragments' );
}, 11 );

You will know it worked when the wc-ajax=get_refreshed_fragments request no longer appears in the network tab on a blog post or About page.

Measuring cache hit ratio, not just "is it on"

Once a single page is confirmed as a cache hit, the next question is whether the hit rate is high across the site as a whole. A plugin that works on the homepage and misses on 80% of the long tail produces the same slow-site symptom as no caching at all.

Where to look depends on what you are running:

  • WP Rocket: no first-party hit rate dashboard. Use server access logs.
  • WP Super Cache: the plugin's own Cache Contents page shows the number of cached files but not the hit/miss ratio.
  • W3 Total Cache: the Dashboard tab shows a live request counter with cache hits and misses when the "statistics" feature is enabled.
  • LiteSpeed Cache: if LSCache is active on a LiteSpeed server, the Dashboard in the plugin shows crawler and cache stats.
  • nginx fastcgi_cache: the cache hit rate is visible in the access log if you add $upstream_cache_status to the log format:
log_format cache_log '$remote_addr - $upstream_cache_status [$time_local] '
                     '"$request" $status $body_bytes_sent';
access_log /var/log/nginx/access.log cache_log;

Then the cache status (HIT, MISS, BYPASS, EXPIRED, STALE, UPDATING, REVALIDATED) appears on every request line, and you can count:

awk '{print $4}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

A healthy WooCommerce content mix typically sits between 70% and 90% HIT on the full log. Anything below 50% is worth investigating, and anything close to zero is one of the causes in this article.

  • Cloudflare: the Caching analytics dashboard shows the cache hit ratio at the edge, separate from the origin cache. A high edge hit rate and a low origin hit rate means Cloudflare is doing the work and the origin plugin is largely redundant; a low edge hit rate means the edge is forwarding most traffic back to the origin and you should look at Cloudflare's page rules and the cache headers your site is returning.

When to escalate

If the checks in this article do not point to a cause, collect the following before reaching out for help. It is what a hosting or performance engineer needs on the first message:

  • The response headers from https://yoursite.nl/sample-page/ loaded twice (use the Network tab in DevTools, or curl -sI if you have SSH access), showing both the first and second request headers
  • The exact caching plugin name and version (Plugins, Installed, copy the version number)
  • WooCommerce version if applicable, and whether HPOS is enabled (Settings, Advanced, Features)
  • WordPress and PHP versions (Tools, Site Health, Info, Server)
  • The list of cookies set on a fresh anonymous visit to the homepage (DevTools, Application, Cookies)
  • The response headers on a URL with UTM parameters and the same URL without
  • Whether a CDN is in front of the site and which one (Cloudflare, Fastly, Bunny)
  • The plugin's exclusion list and any custom bypass rules
  • The server-level cache configuration (nginx fastcgi_cache directives or Varnish VCL) if you run your own server

The hosting provider's first question is going to be some subset of this. Having it ready shortens the back-and-forth from days to hours.

How to prevent recurrence

  • Review the cache exclusion list after any plugin install or major update. Many plugins silently add their own exclusions and do not remove them on deactivation.
  • Re-check the hit rate after every marketing campaign launch. UTM-driven traffic surfaces query-string fragmentation the moment you use it.
  • Keep WooCommerce updated so the session cookie improvements (10.3+) and the cart-fragment changes (7.8+) are active.
  • Do not stack two page cache plugins. Only one can own wp-content/advanced-cache.php. The caching concept article explains why this consistently fails.
  • If you run a store with real traffic, install a persistent object cache. Page caching skips the work. Object caching makes the work faster when it has to run, which is exactly what a logged-in or uncached page needs. The WooCommerce slowness article explains why the uncacheable surface of a store makes object caching disproportionately valuable.

Done chasing slowdowns?

Performance issues tend to come back after quick fixes. WordPress maintenance keeps updates, caching and limits consistent.

Have your WordPress site maintained

Search this site

Start typing to search, or browse the knowledge base and blog.