Some product queries in WooCommerce are expensive by nature — “top 10 best sellers this month” or “related products by shared attributes” both require joins and aggregation that don’t get meaningfully faster just from indexing. Transients let you cache the result of an expensive query rather than trying to make the query itself faster.
A basic cached query
function get_best_sellers_cached($limit = 10) {
$cache_key = 'best_sellers_' . $limit;
$cached = get_transient($cache_key);
if ($cached !== false) {
return $cached;
}
$products = wc_get_products([
'orderby' => 'popularity',
'limit' => $limit,
'status' => 'publish',
]);
set_transient($cache_key, $products, 6 * HOUR_IN_SECONDS);
return $products;
}
Six hours is a reasonable starting point for something like best-sellers, which doesn’t need to be second-by-second accurate. Adjust based on how often the underlying data actually changes meaningfully.
Invalidating on the right event, not on a timer alone
A pure time-based expiry means the cache can serve stale data for the full duration even after something relevant changes. Clearing it on the actual triggering event is more precise:
add_action('woocommerce_order_status_completed', function() {
delete_transient('best_sellers_10');
});
This clears the cache specifically when an order completes — the event that would actually change what “best selling” means — rather than waiting up to six hours for a stale ranking to expire on its own.
Where transients aren’t the right tool
Anything genuinely per-user — a cart total, a personalized recommendation based on browsing history — shouldn’t be cached this way, since the cache key would need to include a user identifier, defeating a lot of the shared-caching benefit. Transients work best for queries whose result is the same for every visitor: category listings, popular products, filtered archives without personalization.
Checking transients aren’t silently disabled
If Redis or Memcached object caching is active on the site, transients get stored there automatically and this all works as expected. Without an external object cache, transients fall back to the wp_options table — which works, but circles back to the autoload bloat problem covered elsewhere, so it’s worth checking transients aren’t set to autoload if they’re large or numerous.