Best Practices
Guidelines and recommendations for using @warlock.js/cache effectively.
Do’s ✅
Section titled “Do’s ✅”Use remember() for Expensive Operations
Section titled “Use remember() for Expensive Operations”Always use remember() for database queries, API calls, or expensive computations to prevent cache stampedes:
// ✅ GOOD: Prevents stampedesconst posts = await cache.remember('posts.popular', 3600, async () => { return await db.query('SELECT * FROM posts ORDER BY views DESC LIMIT 10');});
// ❌ BAD: Vulnerable to stampedeslet 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);}Use Tags for Related Data
Section titled “Use Tags for Related Data”Group related cache entries with tags for easy invalidation:
// ✅ GOOD: Use tags for related dataconst 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 onceawait cache.tags([`user.${userId}`]).invalidate();
// ❌ BAD: Manual key trackingawait cache.set(`user.${userId}.profile`, profileData);await cache.set(`user.${userId}.posts`, postsData);// Later: remember all keys to invalidate?Set Appropriate TTLs
Section titled “Set Appropriate TTLs”Choose TTLs based on data freshness requirements:
import { CACHE_FOR } from '@warlock.js/cache';
// Frequently changing data: Short TTLawait cache.set('current.stats', stats, CACHE_FOR.HALF_HOUR);
// Semi-static data: Medium TTLawait cache.set('user.profile', profile, CACHE_FOR.ONE_DAY);
// Rarely changing data: Long TTLawait cache.set('app.config', config, CACHE_FOR.ONE_WEEK);Use Atomic Operations for Counters
Section titled “Use Atomic Operations for Counters”Always use increment() / decrement() for counters instead of manual get/set:
// ✅ GOOD: atomic, race-condition freeawait cache.increment('page.views', 1);
// ❌ BAD: race condition riskconst 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 objectawait cache.merge<User>(`user.${id}`, { lastSeenAt: Date.now() });
// ✅ Full read-modify-write with the current valueawait 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 lockconst 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.
Monitor with Events
Section titled “Monitor with Events”Set up event listeners for monitoring and debugging:
// ✅ GOOD: Monitor cache performancecache.on('hit', () => metrics.increment('cache.hit'));cache.on('miss', () => metrics.increment('cache.miss'));cache.on('error', ({ error }) => logger.error('Cache error', error));Use Namespaces for Organization
Section titled “Use Namespaces for Organization”Organize cache keys hierarchically with namespaces:
// ✅ GOOD: Organized with namespacesawait 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 manageawait cache.set('user_123_profile', profileData);await cache.set('user_123_settings', settingsData);Set Memory Limits
Section titled “Set Memory Limits”Use maxSize for memory driver to prevent unbounded growth:
// ✅ GOOD: Prevents memory leakscache.setCacheConfigurations({ drivers: { memory: MemoryCacheDriver }, options: { memory: { maxSize: 1000, // Auto-evicts when full ttl: 3600 } }});Use Redis for Distributed Systems
Section titled “Use Redis for Distributed Systems”Always use Redis driver in production with multiple servers:
// ✅ GOOD: Shared cache across serverscache.setCacheConfigurations({ default: 'redis', drivers: { redis: RedisCacheDriver }, options: { redis: { host: process.env.REDIS_HOST, port: 6379 } }});Don’ts ❌
Section titled “Don’ts ❌”Don’t Cache Everything
Section titled “Don’t Cache Everything”Only cache data that benefits from caching:
// ❌ BAD: Caching already fast operationsawait cache.set('simple.calculation', 1 + 1, 3600);
// ✅ GOOD: Cache expensive operationsawait cache.remember('expensive.query', 3600, async () => { return await db.complexAggregation();});Don’t Use Infinite TTL Without Reason
Section titled “Don’t Use Infinite TTL Without Reason”Avoid forever() unless data truly never changes:
// ❌ BAD: Infinite cache for changing dataawait cache.forever('user.stats', stats); // Stats change!
// ✅ GOOD: Reasonable TTLawait cache.set('user.stats', stats, CACHE_FOR.ONE_HOUR);
// ✅ GOOD: Forever for truly static dataawait cache.forever('app.version', '1.0.0');Don’t Ignore Memory Limits
Section titled “Don’t Ignore Memory Limits”Set maxSize for memory driver in production:
// ❌ BAD: Unbounded memory growthcache.setCacheConfigurations({ options: { memory: { // No maxSize - can grow infinitely! } }});
// ✅ GOOD: Bounded memorycache.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 Redisawait 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 appcache.setCacheConfigurations({ default: 'file', // Each server has its own cache!});
// ✅ GOOD: Redis for distributedcache.setCacheConfigurations({ default: 'redis' // Shared across all servers});Performance Tips
Section titled “Performance Tips”Use has() Instead of get()
Section titled “Use has() Instead of get()”When you only need to check existence:
// ✅ GOOD: More efficientif (await cache.has('key')) { const value = await cache.get('key');}
// ❌ BAD: Unnecessary value fetchconst value = await cache.get('key');if (value !== null) { // ...}Batch Operations
Section titled “Batch Operations”Use many() and setMany() for multiple operations:
// ✅ GOOD: Single round-tripconst values = await cache.many(['key1', 'key2', 'key3']);
// ❌ BAD: Multiple round-tripsconst value1 = await cache.get('key1');const value2 = await cache.get('key2');const value3 = await cache.get('key3');Primitives are Fast
Section titled “Primitives are Fast”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 fastawait cache.set('user', { name: 'John' });Choose the Right Driver
Section titled “Choose the Right Driver”| Use Case | Recommended Driver |
|---|---|
| Development | Memory |
| Testing | Null |
| Single server | Memory or File |
| Distributed apps | Redis |
| Persistence + you already run Postgres | Pg (KV mode) |
| Similarity / RAG / semantic caching | Pg (with vector config) — production. Memory drivers — dev only. |
| Memory-constrained | LRU Memory |
| Sliding expiration | Memory Extended |
Tag Organization Strategies
Section titled “Tag Organization Strategies”Strategy 1: Hierarchical Tags
Section titled “Strategy 1: Hierarchical Tags”// User-specific + collectioncache.tags([`user.${id}`, 'users']);
// Resource-specific + collectioncache.tags([`post.${id}`, 'posts', `category.${catId}`]);Strategy 2: Functional Tags
Section titled “Strategy 2: Functional Tags”// By featurecache.tags(['dashboard', 'homepage']);
// By data typecache.tags(['config', 'settings']);Strategy 3: Multi-Dimensional
Section titled “Strategy 3: Multi-Dimensional”// Multiple dimensionscache.tags([ `user.${userId}`, `category.${catId}`, 'featured' // if applicable]);Event Monitoring Patterns
Section titled “Event Monitoring Patterns”Pattern 1: Metrics Collection
Section titled “Pattern 1: Metrics Collection”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);Pattern 2: Debug Logging
Section titled “Pattern 2: Debug Logging”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}`); });}Pattern 3: Error Tracking
Section titled “Pattern 3: Error Tracking”cache.on('error', ({ error, driver, key }) => { sentry.captureException(error, { tags: { cache_driver: driver, cache_key: key } });});Memory Management
Section titled “Memory Management”Set maxSize Appropriately
Section titled “Set maxSize Appropriately”options: { memory: { maxSize: 1000, // Prevents unbounded growth }}Monitor Cache Size with Events
Section titled “Monitor Cache Size with Events”cache.on('set', () => { // Track cache size});
cache.on('removed', () => { // Track cache size});Security Best Practices
Section titled “Security Best Practices”Use Global Prefix
Section titled “Use Global Prefix”Prevent key collisions between applications:
options: { redis: { globalPrefix: process.env.APP_NAME || 'myapp' }}Sanitize Keys
Section titled “Sanitize Keys”Keys are automatically sanitized, but be aware:
// Use dot notation directly instead of objectsawait cache.set("user.1.profile", userData);await cache.get("user.1.profile");Don’t Cache Sensitive Data
Section titled “Don’t Cache Sensitive Data”Avoid caching sensitive information without encryption:
// ❌ BAD: Caching sensitive dataawait cache.set('user.password', password);
// ✅ GOOD: Cache only non-sensitive dataawait cache.set('user.profile', { name: user.name, email: user.email // No password!});Common Patterns
Section titled “Common Patterns”Pattern 1: Cache-Aside
Section titled “Pattern 1: Cache-Aside”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;}Pattern 2: Cache-Aside with remember()
Section titled “Pattern 2: Cache-Aside with remember()”async function getUser(id: number) { return await cache.remember(`user.${id}`, 3600, async () => { return await db.users.findById(id); });}Pattern 3: Write-Through
Section titled “Pattern 3: Write-Through”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;}Pattern 4: Tag-Based Invalidation
Section titled “Pattern 4: Tag-Based Invalidation”// Cache with tagsconst tagged = cache.tags([`user.${id}`, 'users']);await tagged.set(`user.${id}`, userData);
// Invalidate on updateasync function updateUser(id: number, data: any) { await db.users.update(id, data); await cache.tags([`user.${id}`]).invalidate();}Similarity Retrieval
Section titled “Similarity Retrieval”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 + pgvectoroptions: { 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.
Troubleshooting
Section titled “Troubleshooting”- High memory usage? Set
maxSizeor 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 similarityCacheUnsupportedErrorfromsimilar()? The current driver doesn’t index vectors. Check the capability matrix.
Summary
Section titled “Summary”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