This version is in beta. Some features may change before release.

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

Code
rust
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.

cacheinvalidationperformance