Cache Management
Learn how to manage cache invalidation and cleanup.
Recommended invalidation pattern
When data changes in your database, you should invalidate the corresponding cache entries to maintain consistency.
async function deletePost(id: number) {
await db.posts.delete(id);
await cache.invalidateEntity('post', id);
// ↑ atomically removes from every indexed list with trackMembership
}
async function updatePost(id: number, patch: Partial<Post>) {
const updated = await db.posts.update(id, patch);
await cache.updateEntityIfExists('post', updated);
// ↑ refreshes the cache only if the entity was already cached
}Invalidating plain lists
For plain list schemas (no membership index), you must explicitly invalidate any list keys that reference the deleted entity:
await cache.deleteList('recentPosts', {});
await cache.deleteList('userPosts', { userId });Recommendation
This is the main reason to prefer indexedList with trackMembership: true for any list that tracks deletable entities.
invalidateEntity
The killer method. Atomically:
- Removes the entity key
- Looks up every indexed list with
trackMembership: truethat contains this id - Removes the id from all of those lists in one Lua script
const cleaned = await cache.invalidateEntity('post', 42);
// → 3 (removed from post:42 + 3 feeds)Why use invalidateEntity over deleteEntity?
Use this whenever a row is deleted in your DB. Even if you're not yet using trackMembership, calling invalidateEntity is the forward-compatible choice; it costs the same as deleteEntity when no membership exists.
clearAllCache
Destructive. Wipes all keys owned by the engine. Behaviour depends on whether redis.keyPrefix is set.
| Config | What runs | Scope |
|---|---|---|
keyPrefix set | SCAN + UNLINK in batches of 500 | Only keys starting with the prefix; other apps/envs on the same DB untouched |
keyPrefix empty | FLUSHDB | Wipes the entire selected Redis DB. Only use when this cache owns the whole DB |
Safety guards
Two safeguards apply in both modes:
- The caller must pass
{ confirm: 'YES_WIPE_ALL' }exactly - When
safety.productionModeis true, also pass{ allowProduction: true }
// Dev / test only
await cache.clearAllCache({ confirm: 'YES_WIPE_ALL' });
// Production (rare; usually a script, not application code)
await cache.clearAllCache({
confirm: 'YES_WIPE_ALL',
allowProduction: true,
});Destructive operation
Without these safeguards, the call throws InvalidOperationError. This makes it much harder to accidentally wipe a database from a test harness or a mis-targeted CLI invocation.
Why SCAN + UNLINK instead of FLUSHDB?
- FLUSHDB is synchronous on the Redis server and blocks other traffic on large keyspaces. UNLINK releases memory asynchronously in a background thread.
- FLUSHDB deletes everything in the DB, including keys owned by other applications or environments sharing that DB. Scoped delete touches only this cache's keys.
- SCAN is cursor-based and non-blocking; Redis guarantees it won't miss keys that exist for the full duration of the scan.
Key namespacing
All reads, writes, and Lua-script invocations go through ioredis' keyPrefix option, which transparently prepends the configured prefix to every key on the wire.
// Without prefix:
post:42 → stored as 'post:42' in Redis
// With keyPrefix: 'myapp:':
post:42 → stored as 'myapp:post:42' in RedisTrailing : is automatic
If you pass a non-empty prefix that doesn't end in :, the cache appends one for you so the prefix and the entity type don't run together. keyPrefix: 'myapp' is silently normalized to 'myapp:', producing keys like myapp:post:42 rather than the accidental myapppost:42. Pass the trailing : yourself if you want any other separator character.
Migrating to a prefix on an existing DB
If you turn on a prefix against an existing database, old (unprefixed) keys become invisible to the cache and will eventually expire via their TTLs. To delete them immediately, run one FLUSHDBwith the prefix disabled before cutting over. There is no in-place "rename" for existing keys — Redis doesn't support that at scale.
You do not change your schema or your call sites; the prefix is applied per client, so post:42 becomes myapp:post:42 in Redis without any code change.
When to set a prefix
- Sharing a Redis instance with other applications
- Multiple environments (dev, staging, prod) on the same Redis
- Multi-tenant applications where each tenant needs isolation
- Any scenario where you need scoped cache clearing via
clearAllCache
Production best practice
Always set a keyPrefix in production. This enables safe scoped cache clearing and prevents accidental key collisions with other applications.