Skip to content

Instantly share code, notes, and snippets.

@vapvarun
Created April 10, 2026 11:31
Show Gist options
  • Select an option

  • Save vapvarun/367e2c14913235924db676b376fb78ee to your computer and use it in GitHub Desktop.

Select an option

Save vapvarun/367e2c14913235924db676b376fb78ee to your computer and use it in GitHub Desktop.
WooCommerce Caching Strategies That Work with Dynamic Cart Data (woocustomdev.com)
## nginx.conf — Page cache with WooCommerce cart/session exclusions
## Place inside your server {} block (or FastCGI Cache config)
fastcgi_cache_path /tmp/nginx-cache levels=1:2 keys_zone=WORDPRESS:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
server {
# ... your existing server config ...
set $skip_cache 0;
# Skip cache for POST requests
if ($request_method = POST) {
set $skip_cache 1;
}
# Skip cache when query string is present
if ($query_string != "") {
set $skip_cache 1;
}
# Skip cache for WooCommerce dynamic pages
if ($request_uri ~* "(/cart|/checkout|/my-account|/wc-api|/addons|/store|/shop.*add-to-cart=)") {
set $skip_cache 1;
}
# Skip cache when WooCommerce session cookies are present
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|woocommerce_items_in_cart|woocommerce_cart_hash|wp_woocommerce_session") {
set $skip_cache 1;
}
location ~ \.php$ {
fastcgi_cache WORDPRESS;
fastcgi_cache_valid 200 60m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
add_header X-FastCGI-Cache $upstream_cache_status;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
<?php
/**
* wp-config.php additions — Redis object cache configuration
* Requires: Redis server + redis PHP extension (or predis) + wp-redis drop-in
*
* Install the drop-in:
* wp plugin install wp-redis --activate
* wp redis enable
*/
// Redis connection settings
define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );
define( 'WP_REDIS_DATABASE', 0 );
define( 'WP_REDIS_TIMEOUT', 1 );
define( 'WP_REDIS_READ_TIMEOUT', 1 );
// Prefix all keys with the site name to support multi-site Redis instances
define( 'WP_REDIS_PREFIX', 'woocustomdev_' );
// Groups that should NEVER be cached in Redis (volatile WooCommerce data)
define( 'WP_REDIS_IGNORED_GROUPS', [
'counts',
'plugins',
'wc_session_id',
'wc_cart_',
] );
// Selective group flushing: flush only wc_* keys on cart updates
// (requires object-cache.php that supports group-level flushing, e.g., wp-redis 1.7+)
define( 'WP_REDIS_SELECTIVE_FLUSH', true );
<?php
/**
* Fragment caching for WooCommerce — cache expensive partial outputs as transients.
*
* Use case: cache a "Featured Products" section that runs 4-5 DB queries,
* while leaving the mini-cart (dynamic) outside the cached fragment.
*/
/**
* Output a cached product loop fragment.
*
* @param string $fragment_key Unique identifier for this fragment.
* @param callable $callback The expensive rendering callback.
* @param int $expiry Cache duration in seconds (default 1 hour).
*/
function wcd_fragment_cache( string $fragment_key, callable $callback, int $expiry = HOUR_IN_SECONDS ): void {
$transient_key = 'wcd_frag_' . md5( $fragment_key );
$cached = get_transient( $transient_key );
if ( false !== $cached ) {
echo $cached; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return;
}
ob_start();
$callback();
$output = ob_get_clean();
set_transient( $transient_key, $output, $expiry );
echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Invalidate a fragment when a product is saved.
* Hooked to save_post_product so stale fragments are purged immediately.
*/
add_action( 'save_post_product', function ( int $post_id ): void {
// Bust the featured products fragment whenever any product is updated.
delete_transient( 'wcd_frag_' . md5( 'featured_products' ) );
} );
// --- Usage example in a theme template ---
wcd_fragment_cache(
'featured_products',
function (): void {
$products = wc_get_products( [
'featured' => true,
'limit' => 8,
'status' => 'publish',
] );
foreach ( $products as $product ) {
echo '<div class="product-card">';
echo '<a href="' . esc_url( $product->get_permalink() ) . '">';
echo esc_html( $product->get_name() );
echo '</a>';
echo '</div>';
}
}
);
<?php
/**
* WooCommerce AJAX cart handler — always bypasses page/fragment cache.
*
* Pattern: Register an AJAX endpoint that returns fresh cart HTML,
* then update the DOM via JS. This keeps the rest of the page cached
* while the cart stays accurate on every request.
*/
/**
* Register the AJAX action for both logged-in and guest users.
*/
add_action( 'wp_ajax_wcd_get_cart_fragment', 'wcd_get_cart_fragment_handler' );
add_action( 'wp_ajax_nopriv_wcd_get_cart_fragment', 'wcd_get_cart_fragment_handler' );
/**
* Return JSON with fresh cart HTML and item count.
* Sets no-cache headers so proxies / CDNs never cache this response.
*/
function wcd_get_cart_fragment_handler(): void {
// Hard no-cache headers — CDN / Varnish must NOT cache this endpoint.
nocache_headers();
header( 'Surrogate-Control: no-store' );
WC()->cart->calculate_totals();
ob_start();
woocommerce_mini_cart();
$mini_cart_html = ob_get_clean();
wp_send_json_success( [
'cart_html' => $mini_cart_html,
'cart_count' => WC()->cart->get_cart_contents_count(),
'cart_total' => WC()->cart->get_cart_total(),
] );
}
/**
* Enqueue the JS that polls for fresh cart data.
* Uses wp_localize_script to pass nonce + ajaxurl safely.
*/
add_action( 'wp_enqueue_scripts', function (): void {
wp_enqueue_script(
'wcd-cart-refresh',
get_template_directory_uri() . '/assets/js/cart-refresh.js',
[ 'jquery', 'wc-cart-fragments' ],
'1.0.0',
true
);
wp_localize_script( 'wcd-cart-refresh', 'wcdCart', [
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'action' => 'wcd_get_cart_fragment',
'nonce' => wp_create_nonce( 'wcd_cart_nonce' ),
] );
} );
<?php
/**
* Targeted cache invalidation for WooCommerce.
*
* Instead of flushing everything on every action, these hooks
* purge only the URLs and object-cache groups that actually changed.
*/
/**
* Purge the product page cache when stock changes.
* Works with Nginx FastCGI cache and WP Rocket / LiteSpeed / W3TC.
*/
add_action( 'woocommerce_product_set_stock', function ( WC_Product $product ): void {
$url = get_permalink( $product->get_id() );
// WP Rocket
if ( function_exists( 'rocket_clean_post' ) ) {
rocket_clean_post( $product->get_id() );
}
// LiteSpeed Cache
if ( class_exists( 'LiteSpeed_Cache_API' ) ) {
LiteSpeed_Cache_API::purge_post( $product->get_id() );
}
// W3 Total Cache
if ( function_exists( 'w3tc_pgcache_flush_post' ) ) {
w3tc_pgcache_flush_post( $product->get_id() );
}
// Object cache: clear product transients
wc_delete_product_transients( $product->get_id() );
// Optional: hit a custom Nginx purge endpoint
if ( defined( 'WCD_NGINX_PURGE_URL' ) ) {
wp_remote_get( WCD_NGINX_PURGE_URL . str_replace( home_url(), '', $url ) );
}
} );
/**
* Flush shop/category pages when a product status changes
* (e.g., published → draft drops it from archive listings).
*/
add_action( 'transition_post_status', function ( string $new, string $old, WP_Post $post ): void {
if ( 'product' !== $post->post_type || $new === $old ) {
return;
}
// WP Rocket: flush the shop archive and all category pages
if ( function_exists( 'rocket_clean_term' ) ) {
$terms = get_the_terms( $post->ID, 'product_cat' );
if ( is_array( $terms ) ) {
foreach ( $terms as $term ) {
rocket_clean_term( $term, 'product_cat' );
}
}
}
// Bust shop page object cache
wp_cache_delete( 'woocommerce_product_ids', 'woocommerce' );
}, 10, 3 );
/**
* Clear session-scoped object cache groups after checkout.
* Prevents stale order totals showing up in My Account for a brief window.
*/
add_action( 'woocommerce_checkout_order_processed', function ( int $order_id ): void {
wp_cache_delete( 'customer_get_last_order', 'woocommerce' );
wp_cache_flush_group( 'wc_orders' ); // WordPress 6.1+ / Redis 1.8+
wc_delete_shop_order_transients( $order_id );
} );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment