Tag-Based Invalidation
Invalidate related cache entries without knowing their keys.
The Problem
Without tags, you need to track every key that contains user data — easy to miss some.
The Solution
Tag keys when caching, invalidate by tag later.
How Tags Work
Tags are stored as Redis SETs. Each tag maintains a set of cache keys that belong to it.
Redis structure:
cache:_tag:users → SET { "cache:user:1", "cache:user:2", "cache:users:list:1" }
cache:_tag:user:123 → SET { "cache:user:123" }
cache:_tag:org:456 → SET { "cache:user:123", "cache:user:789" }2
3
4
When you invalidate a tag, the service:
- Gets all cache keys from the tag SET (
SMEMBERS) - Deletes those keys from both L1 (memory) and L2 (Redis)
- Deletes the tag SET itself
INFO
invalidateTags(['a', 'b', 'c']) runs invalidation sequentially per tag (not as a single batch). For high-throughput scenarios, keep the tags list short.
Tag Validation Rules
Tags are validated through the Tag value object. Invalid tags throw CacheError.
| Rule | Constraint |
|---|---|
| Not empty | After trimming, length must be > 0 |
| Lowercase | Tags are automatically lowercased |
| No whitespace | Spaces, tabs, newlines forbidden |
| Allowed characters | a-z0-9, -, _, :, . only |
| Max length | 128 characters (default) |
| Max tags per key | 10 (default, configurable via tags.maxTagsPerKey) |
WARNING
Tags are lowercased automatically. User:123 becomes user:123. Keep this in mind when invalidating — always use lowercase tag names.
Basic Usage
With @Cached Decorator
// Tag on cache
@Cached({
key: 'user:{0}',
tags: (id: string) => [`user:${id}`, 'users'],
})
async getUser(id: string): Promise<User> {
return this.repository.findOne(id);
}
// Invalidate by tag
@InvalidateTags({
tags: (id: string) => [`user:${id}`, 'users'],
when: 'after',
})
async updateUser(id: string, data: UpdateDto): Promise<User> {
return this.repository.update(id, data);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
With @InvalidateOn (Distributed)
// Invalidate locally + publish to other nodes
@InvalidateOn({
events: ['user.deleted'],
tags: (result, [id]) => [`user:${id}`, 'users'],
publish: true,
})
async deleteUser(id: string): Promise<void> {
await this.repository.delete(id);
}2
3
4
5
6
7
8
9
With @Cacheable/@CacheEvict Decorators
Requires DeclarativeCacheInterceptor — works in controller context only.
import { Controller, Delete, Get, Param, UseInterceptors } from '@nestjs/common';
import { Cacheable, CacheEvict, DeclarativeCacheInterceptor } from '@nestjs-redisx/cache';
import { User, UserServiceStub } from './types';
@Controller('users')
@UseInterceptors(DeclarativeCacheInterceptor)
export class UserController {
constructor(private readonly userService: UserServiceStub) {}
// Tag on cache
@Cacheable({ key: 'user:{id}', tags: ['users'] })
@Get(':id')
async getUser(@Param('id') id: string): Promise<User> {
return this.userService.findOne(id);
}
// Invalidate by tag (static tags only)
@CacheEvict({ tags: ['users'] })
@Delete(':id')
async deleteUser(@Param('id') id: string): Promise<void> {
return this.userService.delete(id);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
INFO
@CacheEvict supports static tags only (string[]). For dynamic tags based on method arguments, use @InvalidateTags (proxy-based).
With Service API
// Set with tags
await this.cache.set('user:123', user, {
tags: ['users', 'user:123', 'org:456'],
});
// Invalidate single tag
await this.cache.invalidate('users');
// Invalidate multiple tags
await this.cache.invalidateTags(['users', 'products']);
// Invalidate by key pattern (uses Redis SCAN)
await this.cache.invalidateByPattern('user:*');
// Get keys by tag
const keys = await this.cache.getKeysByTag('users');2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Tag Patterns
Entity Tags
// Single entity
@Cached({
key: 'user:{0}',
tags: (id: string) => [`user:${id}`],
})
async getUser(id: string) { }
// Collection
@Cached({
key: 'users:list:{0}',
tags: ['users:list'],
})
async listUsers(page: number) { }2
3
4
5
6
7
8
9
10
11
12
13
Hierarchical Tags
Use hierarchical tags when entities have parent-child relationships. This allows invalidating an entire subtree.
// Cache user with org/team context
@Cached({
key: 'user:{0}',
tags: (id: string, orgId: string, teamId: string) => [
`org:${orgId}`,
`org:${orgId}:team:${teamId}`,
`user:${id}`,
],
})
async getUser(id: string, orgId: string, teamId: string) { }
// Invalidate whole org — clears all users/teams in that org
@InvalidateTags({
tags: (orgId: string) => [`org:${orgId}`],
when: 'after',
})
async deleteOrganization(orgId: string) { }2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Configuration
Tag behavior is configured in plugin options:
new CachePlugin({
tags: {
enabled: true, // Enable/disable tag support (default: true)
indexPrefix: '_tag:', // Prefix for tag index keys in Redis (default: '_tag:')
maxTagsPerKey: 10, // Maximum tags per cache key (default: 10)
ttl: 86400, // TTL for tag index SETs in seconds (default: l2.maxTtl)
},
})2
3
4
5
6
7
8
Tag index TTL
Tag index SETs have their own TTL (default: 24 hours). If a tag SET expires before the cached keys it tracks, those keys become orphans — they won't be found by invalidate() or getKeysByTag(). Set tags.ttl >= your longest cache TTL.
See Configuration for the full options reference.
Best Practices
Do
// Use specific + general tags
tags: ['users', 'user:123']
// Use hierarchical tags for subtree invalidation
tags: ['org:1', 'org:1:team:2', 'org:1:team:2:user:3']
// Keep tags short for high-volume caching
tags: ['u:123']2
3
4
5
6
7
8
Don't
// Don't exceed max tags per key (default: 10, configurable)
// Exceeding throws CacheError, not silently truncated
tags: ['users', 'active', 'premium', 'verified', 'recent', ...]
// Don't put data in tags
tags: [`user:${JSON.stringify(user)}`] // Never!
// Don't use unpredictable tags
tags: [`user:${Date.now()}`] // Can't invalidate later
// Don't use uppercase (lowercased automatically, but be consistent)
tags: ['Users'] // Becomes 'users' — use 'users' directly2
3
4
5
6
7
8
9
10
11
12
Next Steps
- Anti-Stampede — Prevent thundering herd
- Stale-While-Revalidate — Serve stale data while refreshing
- Configuration — Full plugin options reference