Skip to content
Warlock.js v4

Best Practices

Guidelines and recommendations for using @warlock.js/cache effectively.

Always use remember() for database queries, API calls, or expensive computations to prevent cache stampedes:

// ✅ GOOD: Prevents stampedes
const posts = await cache.remember('posts.popular', 3600, async () => {
return await db.query('SELECT * FROM posts ORDER BY views DESC LIMIT 10');
});
// ❌ BAD: Vulnerable to stampedes
let posts = await cache.get('posts.popular');
if (!posts) {
posts = await db.query('SELECT * FROM posts ORDER BY views DESC LIMIT 10');
await cache.set('posts.popular', posts, 3600);
}

Group related cache entries with tags for easy invalidation:

// ✅ GOOD: Use tags for related data
const tagged = cache.tags([`user.${userId}`, 'users']);
await tagged.set(`user.${userId}.profile`, profileData);
await tagged.set(`user.${userId}.posts`, postsData);
// Invalidate all user data at once
await cache.tags([`user.${userId}`]).invalidate();
// ❌ BAD: Manual key tracking
await cache.set(`user.${userId}.profile`, profileData);
await cache.set(`user.${userId}.posts`, postsData);
// Later: remember all keys to invalidate?

Choose TTLs based on data freshness requirements:

import { CACHE_FOR } from '@warlock.js/cache';
// Frequently changing data: Short TTL
await cache.set('current.stats', stats, CACHE_FOR.HALF_HOUR);
// Semi-static data: Medium TTL
await cache.set('user.profile', profile, CACHE_FOR.ONE_DAY);
// Rarely changing data: Long TTL
await cache.set('app.config', config, CACHE_FOR.ONE_WEEK);

Always use increment() / decrement() for counters instead of manual get/set:

// ✅ GOOD: atomic, race-condition free
await cache.increment('page.views', 1);
// ❌ BAD: race condition risk
const views = await cache.get('page.views') || 0;
await cache.set('page.views', views + 1);

For structured (non-numeric) values that need atomic mutation, use update() or merge():

// ✅ Atomic partial update on a cached object
await cache.merge<User>(`user.${id}`, { lastSeenAt: Date.now() });
// ✅ Full read-modify-write with the current value
await cache.update<UserState>(`user.${id}.state`, (current) => ({
...(current ?? defaultState),
visitCount: (current?.visitCount ?? 0) + 1,
}));

For “set only if missing” (distributed locks, idempotency keys), use onConflict: "create":

// ✅ Portable distributed lock
const lock = await cache.set(`lock.${job.id}`, process.pid, {
onConflict: "create",
ttl: "5m",
});
if (!lock.wasSet) throw new Error("Another worker holds the lock");

See Update & Merge and Set Options for details.

Set up event listeners for monitoring and debugging:

// ✅ GOOD: Monitor cache performance
cache.on('hit', () => metrics.increment('cache.hit'));
cache.on('miss', () => metrics.increment('cache.miss'));
cache.on('error', ({ error }) => logger.error('Cache error', error));

Organize cache keys hierarchically with namespaces:

// ✅ GOOD: Organized with namespaces
await cache.set('users.profile.123', profileData);
await cache.set('users.settings.123', settingsData);
await cache.removeNamespace('users'); // Clears all user data
// ❌ BAD: Flat keys, hard to manage
await cache.set('user_123_profile', profileData);
await cache.set('user_123_settings', settingsData);

Use maxSize for memory driver to prevent unbounded growth:

// ✅ GOOD: Prevents memory leaks
cache.setCacheConfigurations({
drivers: { memory: MemoryCacheDriver },
options: {
memory: {
maxSize: 1000, // Auto-evicts when full
ttl: 3600
}
}
});

Always use Redis driver in production with multiple servers:

// ✅ GOOD: Shared cache across servers
cache.setCacheConfigurations({
default: 'redis',
drivers: { redis: RedisCacheDriver },
options: {
redis: {
host: process.env.REDIS_HOST,
port: 6379
}
}
});

Only cache data that benefits from caching:

// ❌ BAD: Caching already fast operations
await cache.set('simple.calculation', 1 + 1, 3600);
// ✅ GOOD: Cache expensive operations
await cache.remember('expensive.query', 3600, async () => {
return await db.complexAggregation();
});

Avoid forever() unless data truly never changes:

// ❌ BAD: Infinite cache for changing data
await cache.forever('user.stats', stats); // Stats change!
// ✅ GOOD: Reasonable TTL
await cache.set('user.stats', stats, CACHE_FOR.ONE_HOUR);
// ✅ GOOD: Forever for truly static data
await cache.forever('app.version', '1.0.0');

Set maxSize for memory driver in production:

// ❌ BAD: Unbounded memory growth
cache.setCacheConfigurations({
options: {
memory: {
// No maxSize - can grow infinitely!
}
}
});
// ✅ GOOD: Bounded memory
cache.setCacheConfigurations({
options: {
memory: {
maxSize: 1000 // Prevents memory leaks
}
}
});

Don’t Cache User-Specific Data in Shared Drivers

Section titled “Don’t Cache User-Specific Data in Shared Drivers”

Be careful with user-specific data in Redis:

// ⚠️ CAUTION: User-specific data in shared Redis
await cache.set(`user.${userId}.session`, sessionData);
// ✅ GOOD: Use global prefix or namespaces
// Configuration:
options: {
redis: {
globalPrefix: () => `tenant.${getTenantId()}`
}
}

Don’t Use File Driver in Distributed Setup

Section titled “Don’t Use File Driver in Distributed Setup”

File driver doesn’t work across multiple servers:

// ❌ BAD: File driver for distributed app
cache.setCacheConfigurations({
default: 'file', // Each server has its own cache!
});
// ✅ GOOD: Redis for distributed
cache.setCacheConfigurations({
default: 'redis' // Shared across all servers
});

When you only need to check existence:

// ✅ GOOD: More efficient
if (await cache.has('key')) {
const value = await cache.get('key');
}
// ❌ BAD: Unnecessary value fetch
const value = await cache.get('key');
if (value !== null) {
// ...
}

Use many() and setMany() for multiple operations:

// ✅ GOOD: Single round-trip
const values = await cache.many(['key1', 'key2', 'key3']);
// ❌ BAD: Multiple round-trips
const value1 = await cache.get('key1');
const value2 = await cache.get('key2');
const value3 = await cache.get('key3');

Primitive values skip cloning overhead:

// ✅ GOOD: Fast (no cloning)
await cache.set('count', 42);
await cache.set('flag', true);
await cache.set('name', 'John');
// Objects/arrays are cloned (security), but still fast
await cache.set('user', { name: 'John' });
Use CaseRecommended Driver
DevelopmentMemory
TestingNull
Single serverMemory or File
Distributed appsRedis
Persistence + you already run PostgresPg (KV mode)
Similarity / RAG / semantic cachingPg (with vector config) — production. Memory drivers — dev only.
Memory-constrainedLRU Memory
Sliding expirationMemory Extended
// User-specific + collection
cache.tags([`user.${id}`, 'users']);
// Resource-specific + collection
cache.tags([`post.${id}`, 'posts', `category.${catId}`]);
// By feature
cache.tags(['dashboard', 'homepage']);
// By data type
cache.tags(['config', 'settings']);
// Multiple dimensions
cache.tags([
`user.${userId}`,
`category.${catId}`,
'featured' // if applicable
]);
let hits = 0, misses = 0;
cache.on('hit', () => hits++);
cache.on('miss', () => misses++);
setInterval(() => {
const total = hits + misses;
const hitRate = total > 0 ? (hits / total) * 100 : 0;
metrics.set('cache.hit_rate', hitRate);
hits = misses = 0; // Reset
}, 60000);
if (process.env.DEBUG_CACHE) {
cache.on('set', ({ key, ttl }) => {
console.log(`[Cache] SET ${key} (TTL: ${ttl}s)`);
});
cache.on('miss', ({ key }) => {
console.log(`[Cache] MISS ${key}`);
});
}
cache.on('error', ({ error, driver, key }) => {
sentry.captureException(error, {
tags: {
cache_driver: driver,
cache_key: key
}
});
});
options: {
memory: {
maxSize: 1000, // Prevents unbounded growth
}
}
cache.on('set', () => {
// Track cache size
});
cache.on('removed', () => {
// Track cache size
});

Prevent key collisions between applications:

options: {
redis: {
globalPrefix: process.env.APP_NAME || 'myapp'
}
}

Keys are automatically sanitized, but be aware:

// Use dot notation directly instead of objects
await cache.set("user.1.profile", userData);
await cache.get("user.1.profile");

Avoid caching sensitive information without encryption:

// ❌ BAD: Caching sensitive data
await cache.set('user.password', password);
// ✅ GOOD: Cache only non-sensitive data
await cache.set('user.profile', {
name: user.name,
email: user.email
// No password!
});
async function getUser(id: number) {
const cached = await cache.get(`user.${id}`);
if (cached) return cached;
const user = await db.users.findById(id);
await cache.set(`user.${id}`, user, 3600);
return user;
}
async function getUser(id: number) {
return await cache.remember(`user.${id}`, 3600, async () => {
return await db.users.findById(id);
});
}
async function updateUser(id: number, data: any) {
const user = await db.users.update(id, data);
await cache.set(`user.${id}`, user, 3600);
await cache.tags([`user.${id}`]).invalidate(); // Also invalidate tags
return user;
}
// Cache with tags
const tagged = cache.tags([`user.${id}`, 'users']);
await tagged.set(`user.${id}`, userData);
// Invalidate on update
async function updateUser(id: number, data: any) {
await db.users.update(id, data);
await cache.tags([`user.${id}`]).invalidate();
}

similar() looks up entries by vector similarity instead of exact key match. Useful for semantic caching, RAG, and “find similar” features. A few rules of thumb:

// ✅ GOOD: production similarity → pg + pgvector
options: {
pg: {
client: pool,
vector: { dimensions: 1536, index: "hnsw" },
},
}
// ✅ GOOD: dev / small datasets → memory family (brute-force)
options: { memory: { ttl: "1h" } }
// ❌ BAD: brute-force memory in production with 100k entries
// Each query is O(N). Switch to pg + pgvector when the dataset grows past
// a few thousand entries.
// ❌ BAD: mixing dimensions
// Vectors are not portable across embedders. Don't switch embedders
// without re-embedding the whole index.

Re-embed when you change embedders. There’s no way to “convert” between vector spaces — different models live in different geometries, and cosineSimilarity will return scores that look reasonable but aren’t comparable.

  • High memory usage? Set maxSize or use LRU driver
  • Low hit rate? Check TTLs are appropriate for your data
  • Stampedes? Use remember() instead of manual get/set
  • Race conditions? Use atomic operations — increment() / decrement() for counters, set(k, v, { onConflict: "create" }) for distributed locks, update() / merge() for structured values
  • Cache not shared? Use Redis driver for distributed systems
  • similar() slow on memory driver? That’s brute force — switch to pg + pgvector for production-scale similarity
  • CacheUnsupportedError from similar()? The current driver doesn’t index vectors. Check the capability matrix.

Key patterns:

  • Use remember() to prevent cache stampedes
  • Use tags for related data invalidation
  • Choose TTLs based on data freshness requirements
  • Use atomic operations for counters
  • Set memory limits on memory drivers
  • Avoid caching sensitive data without encryption
  • Use Redis for distributed systems