There is no quicker way to kill a conversion rate than a broken shopping cart. You are likely reading this because you are experiencing a specific, maddening phenomenon: a customer adds a product to their cart, the page reloads or the drawer opens, and the mini-cart explicitly states "0 items."
For high-performance WooCommerce stores hosted on Cloudways (or any stack utilizing Varnish), this is rarely a code bug in your theme. It is almost always a caching collision.
When Varnish is too aggressive, it serves a static HTML snapshot of the page—one that was generated before the items were added to the cart—rather than the dynamic, user-specific version required for e-commerce.
This guide provides a rigorous root cause analysis and a definitive technical solution to force Varnish to respect WooCommerce cart sessions without sacrificing global site performance.
The Root Cause: How Varnish "Breaks" Dynamic State
To fix the problem, you must understand the architecture of the failure. Varnish is a reverse HTTP proxy. It sits between the user and your web server (Apache/Nginx).
When a request comes in, Varnish checks if it has a saved copy of that page.
- Cache Hit: Varnish serves the HTML instantly from RAM (milliseconds). The request never touches WordPress/PHP.
- Cache Miss: Varnish forwards the request to the backend. WordPress generates the page, sends it back, and Varnish saves it for the next user.
The Cookie Conflict
Varnish logic relies heavily on cookies. By default, if a request contains cookies, Varnish assumes the user has a unique session (like a logged-in admin) and bypasses the cache ("PASS").
However, to achieve high performance, hosting providers strip "non-essential" cookies. If they didn't, every analytics cookie would bust the cache, rendering Varnish useless.
The failure occurs because Varnish is stripping or ignoring the woocommerce_items_in_cart cookie.
WooCommerce uses this specific cookie as a flag. If this cookie is present, the frontend knows to trigger an AJAX request (via wc-ajax=get_refreshed_fragments) to update the mini-cart DOM. If Varnish serves a cached page where the script believes the cart is empty, the AJAX call never fires, or the static HTML overrides the JavaScript update.
The Solution: A Two-Pronged Approach
Fixing this on managed hosting like Cloudways requires two steps: configuration at the server level (via the Platform UI) and enforcement at the application level (via PHP).
Phase 1: Cloudways Varnish Exclusion (The Server Config)
Cloudways allows you to modify Varnish exclusion rules through their application settings. We need to explicitly tell Varnish to bypass the cache if the WooCommerce cart cookie is detected.
- Log in to your Cloudways Platform.
- Navigate to Applications and select your WooCommerce store.
- Click on Application Settings in the left sidebar.
- Select the Varnish Settings tab.
- Click Add New Rule.
We need to add a cookie exclusion. This ensures that any user carrying the "I have items in my cart" cookie gets dynamic content.
Configuration:
- Type: Cookie
- Method: Exclude
- Value:
woocommerce_items_in_cart
Note: You should also ensure wp_woocommerce_session_ is excluded, though Cloudways usually handles this by default.
Save the changes and click Purge Varnish.
Phase 2: The Cache-Busting Enforcer (The Code Fix)
Server rules are sometimes insufficient if the theme or a plugin handles cookies non-standardly (e.g., via JavaScript only, without setting the HTTP header correctly on the first load).
We will implement a strictly typed PHP solution that acts as a failsafe. This code hooks into WordPress headers. If the cart is not empty, it forces a Cache-Control: no-cache header. Varnish respects backend headers; if PHP says "do not cache," Varnish will pass the traffic.
Create a file named varnish-cart-fix.php inside wp-content/mu-plugins/ (create the directory if it doesn't exist). Using an MU-plugin ensures this runs before standard plugins and themes.
<?php
/**
* Plugin Name: WooCommerce Varnish Cart Fix
* Description: Forces cache bypass headers when WooCommerce cart contains items to prevent stale mini-carts.
* Version: 1.0.0
* Author: Engineering Team
*/
declare(strict_types=1);
namespace App\Cache;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class VarnishCartEnforcer {
/**
* Cookie name used by WC to track cart items count.
*/
private const CART_COOKIE = 'woocommerce_items_in_cart';
public function init(): void {
add_action( 'template_redirect', [ $this, 'enforce_cache_headers' ], 1 );
add_action( 'wp_enqueue_scripts', [ $this, 'ensure_cart_fragments' ], 20 );
}
/**
* If the cart is not empty, send headers to kill Varnish caching for this request.
*/
public function enforce_cache_headers(): void {
if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
return;
}
// Check if cart has items via the WC Object
$item_count = WC()->cart->get_cart_contents_count();
// Check for the specific cookie existence as a fallback
$cookie_set = isset( $_COOKIE[self::CART_COOKIE] );
if ( $item_count > 0 || $cookie_set ) {
$this->send_no_cache_headers();
}
}
/**
* Send rigorous Cache-Control headers compatible with Varnish 4/5/6.
*/
private function send_no_cache_headers(): void {
if ( headers_sent() ) {
return;
}
header( 'Cache-Control: no-cache, no-store, must-revalidate, max-age=0' );
header( 'Pragma: no-cache' );
header( 'Expires: Wed, 11 Jan 1984 05:00:00 GMT' ); // Historical WP date
// Specific header often used by Cloudways/Varnish to force a PASS
header( 'X-Do-Not-Cache: true' );
}
/**
* Ensure WooCommerce Cart Fragments are enqueued.
* Modern themes sometimes dequeue this for speed, breaking Varnish compatibility.
*/
public function ensure_cart_fragments(): void {
if ( function_exists( 'is_cart' ) && is_cart() ) {
return;
}
if ( function_exists( 'is_checkout' ) && is_checkout() ) {
return;
}
// If fragments script is missing, AJAX updates to the mini-cart will fail.
if ( ! wp_script_is( 'wc-cart-fragments', 'enqueued' ) ) {
wp_enqueue_script( 'wc-cart-fragments' );
}
}
}
// Initialize
( new VarnishCartEnforcer() )->init();
Deep Dive: Why This Code Works
The template_redirect Hook
We use template_redirect because it is one of the last hooks executed before headers are sent to the browser, but after WooCommerce has loaded the cart session data. This allows us to check WC()->cart->get_cart_contents_count() accurately.
The Headers
Varnish acts on the Cache-Control header coming from the backend.
no-cache: Tells Varnish it must validate the request with the origin server before releasing a cached copy.max-age=0: Tells the browser and Varnish the content is immediately stale.X-Do-Not-Cache: This is a custom header often configured in VCL files on managed hosting environments to explicitly trigger a "PASS" action (bypass cache).
Enqueuing Fragments
In recent years, developers began dequeuing wc-cart-fragments.js to improve Google PageSpeed scores. While this improves the initial load of the homepage, it breaks the mini-cart in cached environments.
The ensure_cart_fragments method in the code above ensures that this critical script remains active unless the user is already on the Cart or Checkout page (where it is redundant).
Common Pitfalls and Edge Cases
1. The "Double Cache" Problem (Cloudflare)
If you use Cloudflare in front of Cloudways, you have two layers of caching. Even if you fix Varnish, Cloudflare might still be caching the HTML. Ensure you have a Page Rule in Cloudflare to bypass cache if the cookie woocommerce_items_in_cart is present, or use the "Cloudflare APO for WordPress" service which handles this logic automatically.
2. WooCommerce 7.8+ and Fragments
WooCommerce is slowly transitioning away from Cart Fragments due to the performance cost of that AJAX call on every page load. They are moving toward "Mini Cart Blocks."
If you are using a Block-based theme (Full Site Editing), the PHP solution above regarding headers (enforce_cache_headers) is still required, but the wc-cart-fragments script enqueue might be unnecessary. Test your specific theme.
3. AJAX Nonces
Sometimes the mini-cart fails not because of caching, but because the cached page contains an expired WordPress Nonce. When the frontend tries to update the cart, the security token is rejected. The solution provided above mitigates this because once the user has items in the cart, the page is dynamically generated (bypassing cache), ensuring a fresh Nonce is always served.
Verification
To verify the fix is working:
- Open your site in an Incognito/Private window.
- Open Chrome DevTools -> Network tab.
- Add an item to the cart.
- Reload the page.
- Click the request for the page HTML (usually the first item in the list).
- Look at the Response Headers.
You should see: X-Cache: MISS (or X-Cache: HIT followed by an immediate reload if fragments are working perfectly). Crucially, if items are in the cart, you should see the Cache-Control: no-cache... headers we injected.
By combining server-side configuration with application-level header enforcement, you ensure that Varnish accelerates your traffic without destroying the user experience.