Skip to content

Cache Tags

Cache tags allow you to group related cache entries and invalidate them together. This makes complex invalidation scenarios simple and error-free.

:::tip Two ways to tag a value You can attach tags to a cached value in two interchangeable ways:

// Fluent — useful when you already have a tagged instance
await cache.tags(["users"]).set("user.1", user);
// Inline — terser when you're writing one value with known tags
await cache.set("user.1", user, { tags: ["users"] });

Both share the same tag index — invalidate them with cache.tags([...]).invalidate() regardless of which shape you used at write time. See Set Options. :::

Without Tags:

// When user updates profile, you need to manually track all cache keys
await cache.remove("users.123.profile");
await cache.remove("users.123.posts");
await cache.remove("users.123.comments");
await cache.remove("feed.homepage"); // if user is featured
// Easy to miss keys, error-prone, hard to maintain

With Tags:

// One line invalidates everything
await cache.tags([`user.123`]).invalidate();
// All related cache entries cleared automatically

When a user updates their profile, invalidate all related cache with a single operation:

import { cache, CACHE_FOR } from "@warlock.js/cache";
// Store user data with tags
const userId = 123;
const userCache = cache.tags(["user", `user.${userId}`]);
await userCache.set(`user.${userId}.profile`, profileData, CACHE_FOR.ONE_HOUR);
await userCache.set(`user.${userId}.preferences`, settingsData, CACHE_FOR.ONE_HOUR);
// When user updates their profile
async function updateUserProfile(userId: number, data: any) {
await db.users.update(userId, data);
// Invalidate all user-related cache
await cache.tags(["user", `user.${userId}`]).invalidate();
}

All keys stored with these tags are invalidated together. Any cache entries tagged with "user" or user.${userId} are removed immediately.

import { cache, CACHE_FOR } from "@warlock.js/cache";
const userId = 123;
const cartCache = cache.tags(["cart", `user.${userId}`]);
// Store cart items
await cartCache.set(`cart.${userId}.items`, cartItems, CACHE_FOR.HALF_HOUR);
await cartCache.set(`cart.${userId}.total`, cartTotal, CACHE_FOR.HALF_HOUR);
// Invalidate cart when user updates it
await cache.tags([`user.${userId}`]).invalidate();
const categoryId = 5;
const productCache = cache.tags(["products", `category.${categoryId}`]);
await productCache.set(`products.category.${categoryId}`, products, CACHE_FOR.ONE_HOUR);
await productCache.set(`products.featured.${categoryId}`, featured, CACHE_FOR.ONE_HOUR);
// Invalidate all products in category when catalog updates
await cache.tags([`category.${categoryId}`]).invalidate();
import { cache, CACHE_FOR } from "@warlock.js/cache";
// User wishlists tagged by user and products
const userId = 123;
const productId = 456;
const wishlistCache = cache.tags([`user.${userId}`, `product.${productId}`, "wishlists"]);
await wishlistCache.set(`wishlist.${userId}.${productId}`, wishlistData, CACHE_FOR.ONE_WEEK);
// Invalidate by user (clear all user's wishlists)
await cache.tags([`user.${userId}`]).invalidate();
// OR invalidate by product (clear all wishlists containing this product)
await cache.tags([`product.${productId}`]).invalidate();
// OR invalidate all wishlists
await cache.tags(["wishlists"]).invalidate();
// Good: Hierarchical tags
cache.tags([`user.${id}`, "users"]);
// Good: Resource-based tags
cache.tags([`post.${postId}`, "posts", `category.${categoryId}`]);
// Avoid: Overly specific tags that duplicate key information
cache.tags([`user.${id}.profile`, `user.${id}.posts`]); // Too specific
const tagged = cache.tags([
`user.${userId}`, // User-specific
"users", // All users
`category.${catId}`, // Category-specific
"posts", // All posts
]);
// When user updates: invalidate user-specific cache
await cache.tags([`user.${userId}`]).invalidate();
// When category updates: invalidate category-specific cache
await cache.tags([`category.${catId}`]).invalidate();
// When all posts need refresh: invalidate all posts
await cache.tags(["posts"]).invalidate();

All standard cache operations work with tagged cache:

import { cache } from "@warlock.js/cache";
const tagged = cache.tags(["users"]);
// Get
const user = await tagged.get("user.123");
// Set
await tagged.set("user.123", userData, 3600);
// Remove (removes key and tag relationships)
await tagged.remove("user.123");
// Has
const exists = await tagged.has("user.123");
// Remember (with stampede prevention)
const user = await tagged.remember("user.123", 3600, async () => {
return await fetchUser(123);
});
// Pull (get and remove)
const temp = await tagged.pull("temp.data");
// Forever (no expiration)
await tagged.forever("config", configData);
// Increment/Decrement
await tagged.increment("user.123.views");

tags([...]).invalidate() emits one invalidated event in addition to its per-key removed events. Use it to record the size and cost of an invalidation:

import { cache } from "@warlock.js/cache";
cache.on("invalidated", ({ tags, keys, durationMs }) => {
metrics.histogram("cache.invalidation.duration", durationMs ?? 0, {
tags: tags?.join(",") ?? "",
});
metrics.count("cache.invalidation.keys", keys?.length ?? 0);
});

The event includes the tags passed to invalidate(), the keys it removed, and the operation duration in milliseconds.

  1. When you store a key with tags, the driver adds the key to each tag’s index with a set primitive (tagAdd), so concurrent tagged writes never drop members.
  2. Each tag keeps the un-prefixed keys associated with it. Redis uses a native SET inside the prefix; Postgres merges into a JSON array on one row; the memory, LRU and mock drivers use a separate store that eviction and expiry never touch. The file driver keeps a serialized list.
  3. When you invalidate tags, the cache reads the members and deletes exactly those, then drops the index.
  4. Tag indexes have no TTL. Each tagged write also prunes up to 20 dead members, and remove() / pull() detach the key from its tags.
  • Storage: Minimal overhead - one additional key per tag per unique key
  • Invalidation: Efficient - O(n) where n is the number of keys per tag
  • Reads: No performance impact - tagged cache reads are as fast as regular cache
  • Memory: Tag relationships stored in cache (uses same driver)
  1. Don’t over-tag: Use 2-4 tags per cache entry, not 10+
  2. Use specific tags: user.123 is better than users when invalidating one user
  3. Group invalidation: Invalidate multiple tags together when possible
  4. Monitor tag counts: If a tag has thousands of keys, invalidation will take longer
  1. Tag relationships stored in cache: Uses the same driver as your cache data. On the memory drivers the index lives in a separate store, so maxSize or LRU capacity can no longer evict it.
  2. Tags are global across scopes: a cache.namespace(...) handle does not isolate tags.
  3. Tags written before 5.17 are orphaned: invalidate can’t reach them; they expire by TTL or flush()
  4. No tag hierarchy: Tags are flat (no parent-child relationships)
  5. Synchronous tag lookup: Tag relationships must be readable for invalidation to work
  • Tags not invalidating? Ensure you’re using the same tag names when storing and invalidating
  • Performance issues? Check if tags have too many keys associated — consider using more specific tags
  • Missing keys after invalidation? Tags invalidate based on stored relationships — ensure keys were stored through tagged cache instances

See Best Practices for more tag organization strategies.