npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nathapp/nestjs-cache

v4.1.0

Published

nestjs-cache

Readme

@nathapp/nestjs-cache

Enterprise-grade caching module for NestJS with multi-tier storage, tag-based invalidation, and distributed synchronization.

Built on top of Cacheable and Keyv for high-performance L1/L2 caching.

Features

  • Multiple strategies: Memory, Distributed (Redis), Multi-Tier (L1 + L2), Legacy
  • Tag-based invalidation: O(m) bulk cache clearing using Redis Sets
  • Distributed sync: Automatic cross-instance cache synchronization via CacheableSync
  • Unified API: Single invalidate() method with mode overloads (key, pattern, tag)
  • Redis URI support: Parse redis:// and rediss:// URIs into connection objects
  • Metrics hooks: Monitor hits, misses, sets, deletes, and errors
  • Health checks: Built-in health check interface
  • Namespace support: Isolated cache namespaces with auto-prefixed tags
  • Graceful degradation: Fallback to memory when Redis is unavailable (MULTI_TIER)
  • Non-blocking mode: Background L2 updates for low-latency responses

Installation

npm install @nathapp/nestjs-cache cacheable @keyv/redis
# For distributed sync (MULTI_TIER with multiple instances):
npm install @qified/redis

Quick Start

Memory Strategy (Single Instance)

import { CacheModule, CacheStrategy } from '@nathapp/nestjs-cache';

@Module({
  imports: [
    CacheModule.register({
      strategy: CacheStrategy.MEMORY,
      memory: {
        lruSize: 1000,
        ttl: '5m',
      },
    }),
  ],
})
export class AppModule {}

Distributed Strategy (Redis Only)

CacheModule.register({
  strategy: CacheStrategy.DISTRIBUTED,
  distributed: {
    host: 'localhost',
    port: 6379,
    ttl: '10m',
  },
})

Multi-Tier Strategy (L1 Memory + L2 Redis)

CacheModule.register({
  strategy: CacheStrategy.MULTI_TIER,
  memory: {
    lruSize: 500,
    ttl: '1m',
  },
  distributed: {
    url: 'redis://localhost:6379',
    ttl: '10m',
  },
})

Redis URI Configuration

CacheModule.register({
  strategy: CacheStrategy.DISTRIBUTED,
  distributed: {
    url: 'redis://username:[email protected]:6379/0',
    ttl: '10m',
  },
})

Individual fields override URI-parsed values:

distributed: {
  url: 'redis://localhost:6379',
  port: 6380,  // Overrides port from URI
}

Usage

Basic Cache Operations

import { Injectable } from '@nestjs/common';
import { CacheManager } from '@nathapp/nestjs-cache';

@Injectable()
export class UserService {
  constructor(private readonly cache: CacheManager) {}

  async getUser(id: string) {
    // Get with resolver (cache-aside pattern)
    return this.cache.get(
      ['USER', id],
      async () => this.userRepository.findById(id),
      60000, // TTL in ms
    );
  }

  async updateUser(id: string, data: UpdateUserDto) {
    await this.userRepository.update(id, data);
    // Invalidate cached user
    await this.cache.invalidate(['USER', id]);
  }
}

Tag-Based Invalidation

Tags allow efficient bulk invalidation of related cache entries. Instead of scanning the entire keyspace (O(n)), tags use Redis Sets for O(m) complexity where m = number of tagged keys.

@Injectable()
export class PostService {
  constructor(private readonly cache: CacheManager) {}

  async getPost(userId: string, postId: string) {
    return this.cache.get(
      ['USER', userId, 'POST', postId],
      async () => this.postRepository.findOne(postId),
      { tags: ['USER:' + userId + ':POSTS', 'USER:' + userId] },
    );
  }

  async setPost(userId: string, postId: string, data: Post) {
    await this.cache.set(
      ['USER', userId, 'POST', postId],
      data,
      60000,
      { tags: ['USER:' + userId + ':POSTS', 'USER:' + userId] },
    );
  }

  async deleteAllUserPosts(userId: string) {
    // Invalidate all posts for a user in one call (O(m))
    await this.cache.invalidate(
      ['USER', userId, 'POSTS'],
      { mode: 'tag' },
    );
  }

  async deleteAllUserData(userId: string) {
    // Invalidate everything for a user
    await this.cache.invalidate(
      ['USER', userId],
      { mode: 'tag' },
    );
  }
}

get() Overloads

// Basic get (returns cached value or null)
const value = await cache.get<User>(['USER', '123']);

// Get with resolver (cache-aside)
const user = await cache.get(['USER', '123'], () => fetchUser('123'));

// Get with resolver and explicit TTL
const user = await cache.get(['USER', '123'], () => fetchUser('123'), 60000);

// Get with resolver and tags (default 300s TTL)
const user = await cache.get(['USER', '123'], () => fetchUser('123'), {
  tags: ['USER:123'],
});

// Get with resolver, TTL, and tags
const user = await cache.get(['USER', '123'], () => fetchUser('123'), 60000, {
  tags: ['USER:123'],
});

Unified invalidate() API

// Exact key deletion (default)
await cache.invalidate(['USER', '123']);

// Explicit mode: exact key
await cache.invalidate(['USER', '123'], { mode: 'key' });

// Tag-based invalidation (O(m))
await cache.invalidate(['USER:123:POSTS'], { mode: 'tag' });

// Backward compatibility: group=true now maps to tag mode
await cache.invalidate(['USER', '123'], true);

Batch Invalidation

await cache.mInvalidate([
  { cacheKey: ['USER', '123'], mode: 'key' },
  { cacheKey: ['USER:456:POSTS'], mode: 'tag' },
  { cacheKey: ['SESSION:789'], mode: 'key' },
]);

Namespaced Cache

const postCache = cache.namespace('POSTS');

// All keys are auto-prefixed: POSTS:123
await postCache.set(['123'], postData, 60000, {
  tags: ['AUTHOR:456'],  // Stored as POSTS:AUTHOR:456
});

// Invalidate within namespace
await postCache.invalidate(['AUTHOR:456'], { mode: 'tag' });

// Invalidate all keys in namespace
await postCache.invalidateAll();

Metrics

CacheModule.register({
  strategy: CacheStrategy.DISTRIBUTED,
  distributed: { host: 'localhost', port: 6379 },
  metrics: {
    onCacheHit: (ctx) => console.log(`HIT ${ctx.key} ${ctx.durationMs}ms`),
    onCacheMiss: (ctx) => console.log(`MISS ${ctx.key}`),
    onCacheSet: (ctx) => console.log(`SET ${ctx.key} ttl=${ctx.ttl}`),
    onCacheDelete: (ctx) => console.log(`DEL ${ctx.key}`),
    onCacheError: (ctx) => console.error(`ERR ${ctx.key}`, ctx.error),
  },
})

Health Check

const health = await cache.isHealthy();
// {
//   healthy: true,
//   timestamp: Date,
//   connections: [
//     { name: 'memory', status: 'connected' },
//     { name: 'redis', status: 'connected', latencyMs: 2 }
//   ],
//   stats: { ... }
// }

Distributed Synchronization (CacheableSync)

For multi-instance deployments using MULTI_TIER, cache synchronization is automatically enabled. When one instance sets or deletes a key, all other instances update their L1 (memory) caches via Redis Pub/Sub.

CacheModule.register({
  strategy: CacheStrategy.MULTI_TIER,
  memory: { ttl: '1m' },
  distributed: { host: 'localhost', port: 6379, ttl: '10m' },
  // sync is enabled by default for MULTI_TIER
  // To disable: sync: { enabled: false }
})

To use a custom namespace for sync isolation:

sync: {
  enabled: true,
  namespace: 'my-service',
}

Tag Configuration

CacheModule.register({
  strategy: CacheStrategy.DISTRIBUTED,
  distributed: { host: 'localhost', port: 6379 },
  tags: {
    enabled: true,          // default: true when Redis available
    tagPrefix: '_TAG:',     // prefix for tag sets in Redis
    tagTtl: 86400,          // tag set TTL in seconds (24h)
    maxTagsPerKey: 10,      // max tags per cache entry
    maxKeysPerTag: 10000,   // safety limit per tag set
  },
})

Cache Interceptor

Automatically cache HTTP responses:

import { CacheInterceptor, CacheReqOptions } from '@nathapp/nestjs-cache';

@Controller('users')
@UseInterceptors(CacheInterceptor)
export class UserController {
  @Get(':id')
  @CacheReqOptions({ ttl: 60000 })
  getUser(@Param('id') id: string) {
    return this.userService.findById(id);
  }
}

Strategies Comparison

| Feature | MEMORY | DISTRIBUTED | MULTI_TIER | LEGACY | |---------|--------|------------|------------|--------| | L1 (Memory) | ✅ | ❌ | ✅ | ✅ | | L2 (Redis) | ❌ | ✅ | ✅ | ❌ | | Tag Invalidation | ❌ | ✅ | ✅ | ⚠️ Limited | | Cross-Instance Sync | N/A | N/A | ✅ Auto | ❌ | | Graceful Degradation | N/A | ✅ | ✅ | N/A | | Non-Blocking | N/A | N/A | ✅ | N/A | | Recommended | Dev/Test | Single Redis | Production | Migration only |

Lifecycle and Resource Ownership

Enhanced strategies (MEMORY, DISTRIBUTED, MULTI_TIER) build a Cacheable instance inside the module. When Nest shuts the application down, CacheModule releases everything it created.

Owned vs. consumer-provided resources

| Resource | Created by | Closed by | |----------|------------|-----------| | Cacheable instance + its Keyv store (@keyv/redis) | @nathapp/nestjs-cache | cacheable.disconnect() invoked by CacheModule on shutdown | | RedisMessageProvider sync subscriber (@qified/redis) | @nathapp/nestjs-cache | Provider disconnect() invoked by CacheModule on shutdown | | invalidator broker clients (LEGACY invalidateOptions.enabled) | @nathapp/nestjs-cache | Invalidation manager close() invoked by CacheModule on shutdown | | External Keyv store / connection you inject yourself | Your application | Your application (CacheModule never touches it) |

cacheable.disconnect() does not close the @qified/redis RedisMessageProvider subscriber, so the module closes it explicitly. MEMORY has no sync provider, and LEGACY has no Cacheable instance, so those disconnects are simply skipped.

Shutdown behavior

  • Runs on OnApplicationShutdown (app.close(), or the shutdown signal).
  • The whole teardown is memoized: repeated/concurrent shutdowns disconnect each owned resource exactly once.
  • Each resource is closed independently. If one disconnect throws or rejects, the remaining resources are still closed and Nest's shutdown is not aborted.
  • Failures are logged with a sanitized message (no credentials or error payloads).

disconnectCacheableInstance() is an internal helper and is not part of the public API surface. Do not depend on it directly.

Migration from Legacy

Before (Legacy)

import { CacheModule } from '@nathapp/nestjs-cache';

CacheModule.register({
  // No strategy = LEGACY by default
  ttl: 300,
})

After (Enhanced)

import { CacheModule, CacheStrategy } from '@nathapp/nestjs-cache';

CacheModule.register({
  strategy: CacheStrategy.MULTI_TIER,
  memory: { lruSize: 1000, ttl: '5m' },
  distributed: { host: 'localhost', port: 6379, ttl: '10m' },
})

The CacheManager API is backward compatible — get(), set(), invalidate() all work the same way with enhanced strategies.

Cache Key Sanitization

Cache key parts are percent-encoded before being joined with :. As of the current release, the non-legacy sanitizer also encodes : itself (: → %3a, uppercased to %3A in resolved keys), so parts containing a colon can no longer collide with different part splits — e.g. ['a:b', 'c'] and ['a', 'b:c'] now resolve to different keys (A%3AB:C vs A:B:C).

⚠️ This changes resolved keys for any part containing :. Existing cache entries built with such parts will miss once (a one-time invalidation / cold cache). If you have pinned keys that must keep their old form, build them with the legacy sanitizer: sanitizeCacheKeyPart(part, true) (legacy mode leaves : untouched). An empty parts array (resolveCacheKey([])) now throws instead of producing an empty key.


Deprecated APIs

⚠️ The following APIs are deprecated and will be removed in a future major release. Please migrate to the recommended alternatives.

InvalidateStrategy.BROKER

Deprecated. Cross-instance cache invalidation is now handled by CacheableSync (built into Cacheable).

// ❌ Deprecated
CacheModule.register({
  invalidate: {
    strategy: InvalidateStrategy.BROKER,
    transport: Transport.REDIS,
    options: { host: 'localhost', port: 6379 },
  },
})

// ✅ Replacement — use MULTI_TIER strategy (sync is automatic)
CacheModule.register({
  strategy: CacheStrategy.MULTI_TIER,
  distributed: { host: 'localhost', port: 6379 },
})

CacheInvalidateManager / RedisCacheInvalidateManager

Deprecated. These classes are replaced by CacheableSync for enhanced strategies. They remain functional only for CacheStrategy.LEGACY.

invalidateByTag() / invalidateByTags()

Deprecated. Use the unified invalidate() API with { mode: 'tag' } instead.

// ❌ Deprecated
await cache.invalidateByTag('USER:123:POSTS');
await cache.invalidateByTags(['USER:123:POSTS', 'USER:456:POSTS']);

// ✅ Replacement
await cache.invalidate(['USER:123:POSTS'], { mode: 'tag' });
await cache.mInvalidate([
  { cacheKey: ['USER:123:POSTS'], mode: 'tag' },
  { cacheKey: ['USER:456:POSTS'], mode: 'tag' },
]);

invalidateByPattern()

Deprecated. Pattern-based invalidation uses O(n) SCAN which doesn't scale. Use tag-based invalidation instead.

// ❌ Deprecated — O(n) keyspace scan
await cache.invalidateByPattern('USER:123:*');

// ✅ Replacement — O(m) tag-based
// First, set entries with tags:
await cache.set(['USER', '123', 'POST', '1'], data, 60000, {
  tags: ['USER:123:POSTS'],
});
// Then invalidate by tag:
await cache.invalidate(['USER:123:POSTS'], { mode: 'tag' });

Note: invalidateByPattern() always deletes matching keys via SCAN in enhanced mode. It does not consult the tag registry, even when one is configured, so a glob pattern is never treated as a literal tag.

buildRedisUrl()

Deprecated. Use resolveRedisConnection() to parse URIs into connection objects instead of building URL strings.

// ❌ Deprecated
const url = buildRedisUrl({ host: 'localhost', port: 6379 });

// ✅ Replacement
const connection = resolveRedisConnection({ host: 'localhost', port: 6379 });

dipatch() (typo alias)

Deprecated. Use the correctly spelled dispatch() method.

lib/inteceptor/ (typo directory)

Deprecated. Imports from lib/inteceptor/ still work via barrel re-export but the implementation has moved to lib/interceptor/.

CacheStrategy.LEGACY

Deprecated. The legacy strategy using @nestjs/cache-manager is maintained for backward compatibility but will be removed in a future major release. Migrate to MEMORY, DISTRIBUTED, or MULTI_TIER.


API Reference

CacheManager

| Method | Description | |--------|-------------| | get<T>(keys, resolver?, ttlOrOptions?, options?) | Get or resolve+cache a value | | getOnly<T>(keys) | Get without resolver | | set<T>(keys, value, ttl?, options?) | Set a value with optional tags | | mset<T>(list) | Set multiple values | | mget(keys) | Get multiple values | | invalidate(key, options?) | Unified invalidation (key/pattern/tag modes) | | mInvalidate(list) | Batch invalidation with mixed modes | | reset() | Clear all cache entries | | touch(keys, ttl?) | Extend TTL of a cached item | | namespace(name) | Create a namespaced cache manager | | isHealthy() | Health check | | getStats() | Cache statistics (enhanced mode) | | isEnhancedMode() | Check if using cacheable | | resolveKey(keyParts) | Resolve key parts to string |

CacheModule

| Method | Description | |--------|-------------| | register(options) | Synchronous registration | | registerAsync(options) | Async registration with factory |

License

See the root monorepo license.