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. 0, false and "" are cached like any other value; only null counts as a miss:
// ✅ 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 the Redis (or Postgres) driver in production with multiple servers. The memory drivers are per-process; see Running on 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});Running on multiple servers
Section titled “Running on multiple servers”When more than one process serves traffic, every process gets its own memory, memoryExtended, lru and file cache. Use redis or pg for anything that must agree across servers.
| Need | Use | Why |
| --- | --- | --- |
| Rate-limit or login-throttle counters | increment() on redis or pg | INCRBY / a single upsert, TTL kept; on memory each server counts separately, so the limit is N× looser |
| One-time tokens | pull() on redis or pg | GETDEL / DELETE … RETURNING hands the value to one caller |
| Shared read-modify-write | update() / merge() on redis or pg | Compare-and-set with retries; keep the callback free of side effects |
| One server runs a job | cache.lock() on redis or pg | Finite TTL required; release only removes your own lock |
| Tag invalidation | redis or pg | The index is shared, so one server’s invalidate() reaches every server’s entries |
Also remember:
- Stampede protection is per process.
remember(),swr()andcached()share one in-flight computation within a process only; N servers can still compute once each on a cold miss. Wrap the work incache.lock()when it must run once globally. - Set a
globalPrefixwhen the Redis database is shared. Without one,flush()runsFLUSHDBand clears the whole database. - Redis Cluster is not supported.
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