Tagged cache invalidation
Cache a computed value under one or more tags with Cache::get_or_compute_tagged, then bust every value under a tag in one call with bust_tag - the primitive behind zero-config media-access caching.
Reach for tagged invalidation when a cached value depends on rows that change independently of any fixed TTL - membership, ownership, a permission flag - and you want to bust exactly the cached values a row's change affects, rather than waiting out a TTL or flushing the whole cache. Cache::get_or_compute_tagged checks the cache first, and on a miss runs your closure and stores the result indexed under the tags you name; Cache::bust_tag deletes every value stored under a tag. TTL still applies underneath as the backstop if you forget to bust.
This is the same primitive StoragePlugin::media_access_cached (see Media access control) uses internally, exposed via the core umbral::cache::TaggedCache trait so any plugin can reach the ambient cache without depending on umbral-cache directly.
Example
use umbral_cache::{Cache, Computed, StoreSpec}; let cache = umbral_cache::ambient().expect("cache initialised at boot"); let is_member: bool = cache .get_or_compute_tagged("membership:42:7", || async move { let allowed = check_membership(42, 7).await; Computed { value: allowed, store: Some(StoreSpec { tags: vec!["channel:7".into()], ttl: None, // rely on the tag bust, not a TTL }), } }) .await; // elsewhere, when membership for channel 7 changes:cache.bust_tag("channel:7").await;Passing store: None from the closure opts that particular value out of caching entirely (useful for a volatile decision you never want to persist).
The bust-during-compute race
A cache miss runs your closure (the DB work) and then stores the result — and a bust_tag can land in that gap, between the miss and the store. A naive store would write the just-computed value after the bust that was meant to invalidate it, leaving a stale value alive until its TTL.
The cache closes this with a guarded store. Capture the cache's monotonic bust epoch before computing, then store through set_tagged_bool_guarded(key, value, ttl, tags, since_epoch): the store is skipped (returning false) if any of the value's tags was busted after that epoch. This is what powers media-access caching (media_access_cached) — a permission revoked while a decision was being computed never gets cached. The guard is per-tag, so a bust of an unrelated tag doesn't force a needless recompute, and the failure mode is always recompute, never serve-stale.
See also
Full design in docs/superpowers/specs/2026-09-20-media-access-redesign-design.md.