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:

  1. Removes the entity key
  2. Looks up every indexed list with trackMembership: true that contains this id
  3. 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.

ConfigWhat runsScope
keyPrefix setSCAN + UNLINK in batches of 500Only keys starting with the prefix; other apps/envs on the same DB untouched
keyPrefix emptyFLUSHDBWipes the entire selected Redis DB. Only use when this cache owns the whole DB

Safety guards

Two safeguards apply in both modes:

  1. The caller must pass { confirm: 'YES_WIPE_ALL' } exactly
  2. When safety.productionMode is 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 Redis

Trailing : 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.