Platform guide
How does Smart Add-Ons object cache invalidation actually work on Redis or Memcached?
Never with a group-wide or site-wide flush — a flushGroup() helper exists in the code but isn't called anywhere. Instead, cached add-on data is invalidated through versioned cache keys: a global-scope group change bumps a version number so every future request reads a new key, while a product- or category-scope change deletes just that one product's cache entry directly. There was a real bug here once: an earlier version cached the visitor's A/B test variant inside the same cache entry, which under a real persistent object cache meant the first visitor's assignment got served to everyone else for up to an hour. It's fixed — A/B resolution now runs fresh on every request, outside the cached read.
Smart Add-Ons object cache invalidation is worth understanding precisely if a store runs Redis or Memcached rather than WordPress’s default single-request object cache — the two behave differently enough that a bug can hide completely on one and break everything on the other, which is exactly what happened here once.

A thin wrapper, and a flush method that’s never called
All caching goes through one small class wrapping the standard WordPress object cache functions under one group name:
class Cache {
private const GROUP = 'saue';
public static function get( string $key ): mixed { return wp_cache_get( $key, self::GROUP ); }
public static function set( string $key, mixed $value, int $ttl = 3600 ): bool { ... }
public static function flushGroup(): void { ... } // defined, never called
}
flushGroup() exists and would fall back to a full wp_cache_flush() — clearing the entire object cache, not just this plugin’s entries — on any caching backend that doesn’t support WordPress’s newer group-flush function. Confirmed by grep: nothing in the plugin ever calls it. That blunt instrument was built and then made unnecessary by a more precise approach.
Smart Add-Ons object cache invalidation: versioned keys instead of flushing
A product’s cache key embeds a version number, not just the product ID:
public static function productCacheKey( int $product_id ): string {
return 'addons:' . $product_id . ':v' . self::globalVersion();
}
When a global-scope group changes — the kind that can apply to every product — bumpGlobalVersion() increments that version number. Nothing gets deleted; every product’s next cache read simply asks for a key with the new version suffix, which doesn’t exist yet, so it recomputes and caches fresh. The old :v1 entries are never explicitly removed — they just age out on their own TTL, unreachable the moment the version bumps. A product- or category-scoped change is narrower, so it doesn’t need a version bump at all — it deletes that one product’s specific cache key directly.
A real bug this caught: A/B variants leaking under Redis
The fix is documented directly in the code’s own comment, and it’s worth quoting in full because the failure mode is genuinely subtle:
// Only the raw, visitor-independent query result is cached. A/B resolution
// depends on the request and must run on every call: it was previously
// computed inside this same cached closure, so under a persistent object
// cache (Redis/Memcached) the first visitor's assigned variant would be
// cached and served to every other visitor for the remaining TTL.
Without a real persistent object cache plugin, wp_cache_* only lives for the duration of a single request — so caching something request-specific inside it was invisible as a bug there. The moment a store adds Redis or Memcached and that cache starts surviving between requests, the exact same code would have silently frozen every visitor onto whichever A/B variant the very first visitor happened to get, for up to an hour at a time, with no error and no obvious symptom beyond a split test that mysteriously stopped splitting.
The fix: only the visitor-independent query result goes into the cached closure. A/B resolution runs fresh on every single request, reading the cached (shared, safe-to-share) group data but applying each visitor’s own variant assignment on top of it every time — the same underlying pattern documented in the cache-safe A/B testing guide for full-page caches, applied here to the separate object-cache layer.
Common questions
Does this matter on a typical shared-hosting site without Redis?
Not for the A/B-leak bug specifically — without a persistent object cache backend installed, wp_cache_* resets on every request, so the bug it fixed could never have manifested there. Smart Add-Ons object cache invalidation still applies and still works correctly either way; it just becomes load-bearing once a real persistent cache is in the picture.
How long is add-on data cached for?
An hour (HOUR_IN_SECONDS) per product cache entry — short enough that even a missed invalidation edge case self-corrects within an hour, on top of the explicit invalidation paths Smart Add-Ons object cache invalidation already covers above.
For the full-page caching layer this object-cache design complements, see the cache-safe A/B testing guide, and WordPress’s own WP_Object_Cache documentation (opens in new tab) for how the underlying wp_cache_* functions behave with and without a persistent backend.
Comments