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

nest-cache-redis

v1.0.0

Published

A simplified, high-performance NestJS Redis cache module with fire-and-forget support, automatic keyPrefix handling, pattern-based deletion, and TypeScript-first design

Readme

nest-cache-redis

npm version License: MIT TypeScript

A simplified, high-performance NestJS Redis cache module using ioredis with fire-and-forget support and debug logging.

Features

Simple API - Clean methods: get, set, getOrSet, delKey, delKeys, delPattern, delPatterns, getKeysByPattern, reset, getStats
🚀 Fire and Forget Mode - Optional non-blocking cache operations (global + method-level override)
🌍 Global Module - Register as global module by default (configurable)
🐛 Debug Logging - Built-in debug mode for cache operations
SCAN instead of KEYS - Non-blocking pattern deletion
⏱️ TTL in Seconds - Uses Redis EX command for better compatibility
🔧 Full ioredis Config - Access all ioredis configuration options
Cache Statistics - Monitor cache performance with built-in stats
📦� TypeScript First - Full type safety and autocomplete

Installation

# npm will automatically install peer dependencies (npm 7+)
npm install nest-cache-redis

Note: This package has the following peer dependencies that will be automatically installed with npm 7+:

  • ioredis (^5.0.0)
  • @nestjs/common (^9.0.0 || ^10.0.0 || ^11.0.0)
  • reflect-metadata (^0.1.13 || ^0.2.0)
  • rxjs (^7.0.0)

Quick Start

1. Import the Module

import { Module } from "@nestjs/common";
import { RedisCacheModule } from "nest-cache-redis";

@Module({
  imports: [
    RedisCacheModule.forRoot({
      redisOptions: {
        host: "localhost",
        port: 6379,
        password: "your-password", // optional
      },
      ttl: 3600, // Default TTL in seconds (optional)
      fireAndForget: false, // Set to true for non-blocking operations
      debug: false, // Set to true to enable debug logging
      isGlobal: true, // Register as global module (default: true)
    }),
  ],
})
export class AppModule {}

2. Use the Service

import { Injectable } from "@nestjs/common";
import { RedisCacheService } from "nest-cache-redis";

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

  async getUser(id: string) {
    // Try to get from cache, or compute and cache
    return this.cache.getOrSet(
      `user:${id}`,
      async () => {
        // This function only runs if cache misses
        const user = await this.db.findUser(id);
        return user;
      },
      3600 // TTL in seconds
    );
  }

  async updateUser(id: string, data: any) {
    await this.db.updateUser(id, data);

    // Invalidate cache
    await this.cache.delKey(`user:${id}`);
  }
}

API Reference

Configuration Options

interface RedisCacheModuleOptions {
  redisOptions?: RedisOptions; // All ioredis options
  ttl?: number; // Default TTL in seconds
  fireAndForget?: boolean; // Non-blocking mode (default: false)
  debug?: boolean; // Enable debug logs (default: false)
  isGlobal?: boolean; // Register as global module (default: true)
}

Methods

get<T>(key: string): Promise<T | null>

Get a value from cache.

const user = await cache.get<User>("user:123");
if (user) {
  console.log("Cache hit!", user);
}

set<T>(key: string, value: T, ttl?: number, options?: { fireAndForget?: boolean }): Promise<void>

Set a value in cache with optional TTL (in seconds). Optionally override global fireAndForget setting.

await cache.set("user:123", { name: "John" }, 3600);

// Override fireAndForget for this operation
await cache.set("analytics:view", data, 60, { fireAndForget: true });

getOrSet<T>(key: string, fn: () => Promise<T> | T, ttl?: number): Promise<T>

Get from cache or compute and cache the result.

const user = await cache.getOrSet(
  "user:123",
  async () => await fetchUserFromDB(123),
  3600
);

delKey(key: string, options?: { fireAndForget?: boolean }): Promise<number>

Delete a single key. Returns the number of keys deleted. Optionally override global fireAndForget setting.

const deleted = await cache.delKey("user:123");
console.log(`Deleted ${deleted} key(s)`);

// Override fireAndForget for this operation
await cache.delKey("temp:data", { fireAndForget: true });

delKeys(keys: string[], options?: { fireAndForget?: boolean }): Promise<number>

Delete multiple keys at once. Optionally override global fireAndForget setting.

await cache.delKeys(["user:1", "user:2", "user:3"]);

// Fire and forget
await cache.delKeys(["temp:1", "temp:2"], { fireAndForget: true });

delPattern(pattern: string): Promise<number>

Delete all keys matching a pattern using SCAN (non-blocking).

Automatically handles keyPrefix: If you configured a keyPrefix in Redis options (e.g., keyPrefix: 'myapp:'), the package automatically adds it when scanning and removes it when deleting. You don't need to include the prefix in your pattern!

// If keyPrefix is 'myapp:', this automatically scans for 'myapp:user:*'
await cache.delPattern("user:*");

// Delete specific pattern
await cache.delPattern("session:2024:*");

// With keyPrefix 'myapp:', actual Redis keys: 'myapp:session:2024:*'
// But you just pass the pattern without prefix!

delPatterns(patterns: string[]): Promise<number>

Delete multiple patterns efficiently.

await cache.delPatterns(["user:*", "session:*", "temp:*"]);
// Each pattern automatically gets the keyPrefix if configured

reset(options?: { fireAndForget?: boolean }): Promise<void>

Clear all cache (flushdb). Optionally override global fireAndForget setting.

await cache.reset(); // ⚠️ Use with caution!

// Fire and forget
await cache.reset({ fireAndForget: true });

getKeysByPattern(pattern: string): Promise<string[]>

Get all keys matching a pattern using SCAN (non-blocking). Returns keys without the prefix.

Automatically handles keyPrefix: Just like delPattern, this method automatically adds the keyPrefix when scanning and removes it from the returned keys.

// Get all user keys (automatically handles keyPrefix)
const userKeys = await cache.getKeysByPattern("user:*");
console.log(userKeys); // ['user:1', 'user:2', 'user:3']
// Note: No prefix in returned keys, even if keyPrefix is configured!

// Get session keys for specific date
const sessionKeys = await cache.getKeysByPattern("session:2024-12:*");

// Useful for debugging or monitoring
const allKeys = await cache.getKeysByPattern("*");
console.log(`Total keys: ${allKeys.length}`);

getStats(): Promise<CacheStats>

Get Redis cache statistics including key count, memory usage, and connection status.

const stats = await cache.getStats();
console.log(stats);
// {
//   connected: true,
//   keyCount: 1250,
//   memoryUsed: '2.5M',
//   memoryPeak: '3.1M',
//   memoryFragmentationRatio: '1.05'
// }

getClient(): Redis

Get the underlying ioredis client for advanced operations.

const client = cache.getClient();
await client.ping(); // Direct ioredis access

Advanced Configuration

Async Configuration

import { RedisCacheModule } from "nest-cache-redis";
import { ConfigModule, ConfigService } from "@nestjs/config";

@Module({
  imports: [
    RedisCacheModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        redisOptions: {
          host: configService.get("REDIS_HOST"),
          port: configService.get("REDIS_PORT"),
          password: configService.get("REDIS_PASSWORD"),
          db: configService.get("REDIS_DB", 0),
          keyPrefix: "myapp:", // Add prefix to all keys
          retryStrategy: (times) => Math.min(times * 50, 2000),
        },
        ttl: 3600,
        fireAndForget: false,
        debug: configService.get("NODE_ENV") === "development",
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

Global vs Non-Global Module

By default, the module is registered as global (available everywhere without re-importing). You can change this:

// Global (default) - available in all modules
RedisCacheModule.forRoot({
  isGlobal: true, // default
  redisOptions: {
    /* ... */
  },
});

// Non-global - must import in each module that needs it
RedisCacheModule.forRoot({
  isGlobal: false,
  redisOptions: {
    /* ... */
  },
});

Full ioredis Configuration

All ioredis options are supported:

RedisCacheModule.forRoot({
  redisOptions: {
    host: "localhost",
    port: 6379,
    password: "secret",
    db: 0,
    keyPrefix: "myapp:",

    // Connection
    connectTimeout: 10000,
    lazyConnect: false,
    keepAlive: 30000,

    // Retry
    retryStrategy: (times) => Math.min(times * 100, 3000),
    maxRetriesPerRequest: 3,

    // TLS
    tls: {
      ca: fs.readFileSync("ca.crt"),
    },

    // Sentinel
    sentinels: [
      { host: "sentinel1", port: 26379 },
      { host: "sentinel2", port: 26379 },
    ],
    name: "mymaster",
  },
  ttl: 3600,
  debug: true,
});

Fire and Forget Mode

When fireAndForget: true, cache operations don't wait for Redis responses:

RedisCacheModule.forRoot({
  fireAndForget: true, // Global non-blocking mode
  redisOptions: { host: "localhost", port: 6379 },
});

// This returns immediately without waiting for Redis
await cache.set("key", "value"); // Fires and forgets

// Note: get() always waits for response
const value = await cache.get("key"); // Always waits

Method-Level Override

You can override the global fireAndForget setting for individual operations:

// Global setting: fireAndForget = false (wait for responses)
RedisCacheModule.forRoot({
  fireAndForget: false,
  redisOptions: {
    /* ... */
  },
});

// In your service:
// Wait for this operation (uses global setting)
await cache.set("important:data", value, 3600);

// Fire and forget for this specific operation
await cache.set("analytics:event", data, 60, { fireAndForget: true });

// Same for delete operations
await cache.delKey("temp:data", { fireAndForget: true });
await cache.delKeys(["temp:1", "temp:2"], { fireAndForget: true });
await cache.reset({ fireAndForget: true });

Use cases:

  • High-throughput applications
  • When cache failures shouldn't block requests
  • Fire-and-forget cache warming
  • Analytics/logging that doesn't need confirmation
  • Temporary data that can be lost

Debug Mode

Enable debug logging to see all cache operations:

RedisCacheModule.forRoot({
  debug: true,
  redisOptions: { host: "localhost", port: 6379 },
});

Example debug output:

[RedisCacheService] DEBUG GET key: user:123 - HIT
[RedisCacheService] DEBUG SET key: user:456, ttl: 3600s
[RedisCacheService] DEBUG DEL pattern: session:* - deleted 25 keys
[RedisCacheService] DEBUG GETORSET key: user:789
[RedisCacheService] DEBUG GETORSET key: user:789 - computing value
[RedisCacheService] DEBUG SET key: user:789, ttl: 3600s

Examples

Cache User Data

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

  async findOne(id: string): Promise<User> {
    return this.cache.getOrSet(
      `user:${id}`,
      () => this.userRepository.findOne(id),
      3600 // 1 hour
    );
  }

  async update(id: string, data: UpdateUserDto): Promise<User> {
    const user = await this.userRepository.update(id, data);

    // Invalidate cache
    await this.cache.delKey(`user:${id}`);

    return user;
  }

  async delete(id: string): Promise<void> {
    await this.userRepository.delete(id);
    await this.cache.delKey(`user:${id}`);
  }
}

Cache API Responses

@Injectable()
export class ApiService {
  constructor(private cache: RedisCacheService) {}

  async fetchData(endpoint: string): Promise<any> {
    const cacheKey = `api:${endpoint}`;

    return this.cache.getOrSet(
      cacheKey,
      async () => {
        const response = await fetch(endpoint);
        return response.json();
      },
      300 // 5 minutes
    );
  }
}

Batch Invalidation

@Injectable()
export class CacheService {
  constructor(private cache: RedisCacheService) {}

  async clearUserCache(userId: string): Promise<void> {
    // Clear all user-related cache
    await this.cache.delPatterns([
      `user:${userId}:*`,
      `profile:${userId}:*`,
      `permissions:${userId}:*`,
    ]);
  }

  async clearAllSessions(): Promise<void> {
    const deleted = await this.cache.delPattern("session:*");
    console.log(`Cleared ${deleted} sessions`);
  }
}

Session Management

@Injectable()
export class SessionService {
  constructor(private cache: RedisCacheService) {}

  async createSession(userId: string, data: any): Promise<string> {
    const sessionId = generateId();
    const key = `session:${sessionId}`;

    await this.cache.set(key, { userId, ...data }, 86400); // 24 hours

    return sessionId;
  }

  async getSession(sessionId: string): Promise<any> {
    return this.cache.get(`session:${sessionId}`);
  }

  async destroySession(sessionId: string): Promise<void> {
    await this.cache.delKey(`session:${sessionId}`);
  }

  async destroyUserSessions(userId: string): Promise<number> {
    // Find and delete all sessions for user
    return this.cache.delPattern(`session:*:${userId}`);
  }
}

Monitor Cache Performance

@Injectable()
export class MonitoringService {
  constructor(private cache: RedisCacheService) {}

  async getCacheHealth(): Promise<any> {
    const stats = await this.cache.getStats();

    return {
      status: stats.connected ? "healthy" : "unhealthy",
      metrics: {
        totalKeys: stats.keyCount,
        memoryUsed: stats.memoryUsed,
        memoryPeak: stats.memoryPeak,
        fragmentationRatio: stats.memoryFragmentationRatio,
      },
      timestamp: new Date().toISOString(),
    };
  }

  async checkCacheLimit(): Promise<void> {
    const stats = await this.cache.getStats();

    if (stats.keyCount > 10000) {
      this.logger.warn(`Cache has ${stats.keyCount} keys - consider cleanup`);
    }

    // Parse memory (e.g., "2.5M" -> 2.5)
    const memoryMB = parseFloat(stats.memoryUsed);
    if (memoryMB > 100) {
      this.logger.warn(
        `Cache using ${stats.memoryUsed} - consider optimization`
      );
    }
  }
}

Best Practices

1. Use Consistent Key Patterns

// Good: namespace:entity:id
user:123
user:123:profile
user:123:permissions
session:abc123
product:456

// Makes pattern deletion easier
await cache.delPattern('user:123:*');

2. Set Appropriate TTLs

// Short-lived data
await cache.set("rate-limit:user:123", count, 60); // 1 minute

// Medium-lived data
await cache.set("user:123", user, 3600); // 1 hour

// Long-lived data
await cache.set("config:app", config, 86400); // 24 hours

3. Handle Cache Misses Gracefully

async getUser(id: string): Promise<User | null> {
  try {
    return await this.cache.getOrSet(
      `user:${id}`,
      () => this.db.findUser(id),
      3600
    );
  } catch (error) {
    // Log error but don't fail request
    this.logger.error('Cache error:', error);
    return this.db.findUser(id); // Fallback to DB
  }
}

4. Use Fire-and-Forget for Non-Critical Cache

// Configure fire-and-forget for specific module
@Module({
  imports: [
    RedisCacheModule.register({
      fireAndForget: true, // Don't block on cache writes
      redisOptions: {
        /* ... */
      },
    }),
  ],
})
export class AnalyticsModule {}

TypeScript Support

Full TypeScript support with generics:

interface User {
  id: string;
  name: string;
  email: string;
}

// Type-safe cache operations
const user = await cache.get<User>("user:123");
if (user) {
  console.log(user.name); // TypeScript knows this is a string
}

await cache.set<User>("user:123", {
  id: "123",
  name: "John",
  email: "[email protected]",
});

const users = await cache.get<User[]>("users:active");

Testing

Mock the cache service in tests:

import { Test } from "@nestjs/testing";
import { RedisCacheService } from "nest-cache-redis";

describe("UserService", () => {
  let service: UserService;
  let cache: RedisCacheService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: RedisCacheService,
          useValue: {
            get: jest.fn(),
            set: jest.fn(),
            getOrSet: jest.fn(),
            delKey: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<UserService>(UserService);
    cache = module.get<RedisCacheService>(RedisCacheService);
  });

  it("should get user from cache", async () => {
    const mockUser = { id: "123", name: "John" };
    jest.spyOn(cache, "get").mockResolvedValue(mockUser);

    const result = await service.getUser("123");

    expect(cache.get).toHaveBeenCalledWith("user:123");
    expect(result).toEqual(mockUser);
  });
});

Troubleshooting

Connection Issues

Enable debug mode to see connection logs:

RedisCacheModule.register({
  debug: true,
  redisOptions: {
    host: "localhost",
    port: 6379,
    retryStrategy: (times) => {
      console.log(`Retry attempt ${times}`);
      return Math.min(times * 100, 3000);
    },
  },
});

Memory Issues

Monitor Redis memory and set maxmemory policy:

# In redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru

Performance Issues

  • Use fireAndForget: true for high-throughput scenarios
  • Use delPattern() instead of multiple delKey() calls
  • Batch operations with delKeys() instead of individual deletes
  • Set appropriate TTLs to prevent memory bloat

License

MIT

Contributing

Contributions welcome! Please open an issue or PR.

Support