Skip to content
NestJS ns caching 4 min read

Redis Cache Store

The default in-memory cache that ships with @nestjs/cache-manager lives inside a single Node process, which means a value cached on one instance is invisible to every other instance and is lost the moment that process restarts. Once you scale horizontally behind a load balancer, that becomes a correctness problem: requests bounce between nodes that each hold a different, stale view of the cache. Swapping the in-memory store for Redis gives every instance a single shared cache, survives restarts and deploys, and lets you reason about TTLs and invalidation centrally.

Why a distributed store matters

CacheModule is store-agnostic. Whatever object you pass as store simply has to implement cache-manager’s get/set/del contract, so moving from memory to Redis is a configuration change rather than a code change in your services. The benefits compound at scale:

  • A cache hit on instance A is also a hit on instances B and C.
  • Restarts, rolling deploys, and autoscaling no longer cold-start an empty cache.
  • TTLs and explicit invalidation are coordinated in one place, not duplicated per process.
  • Memory pressure moves off your application heap and onto Redis, where eviction is tunable.

Installing the Redis store

Modern cache-manager (v5+) uses Keyv adapters under the hood. The recommended pairing for NestJS 10/11 is @keyv/redis plus cacheable, which together give you a robust Redis-backed store.

npm install @nestjs/cache-manager cache-manager @keyv/redis keyv cacheable

Note: Older guides use cache-manager-redis-store or cache-manager-ioredis. Those target cache-manager v4 and do not work with the v5 API that current @nestjs/cache-manager depends on. Prefer the Keyv-based setup below.

Registering Redis asynchronously

Connection details belong in configuration, not in source, so register the module with registerAsync and inject ConfigService. The stores array accepts a Keyv instance wrapping the Redis adapter; the namespace becomes the key prefix Redis sees, which keeps multiple apps (or environments) from colliding on the same Redis instance.

// src/cache/cache.module.ts
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { createKeyv } from '@keyv/redis';
import Keyv from 'keyv';

@Module({
  imports: [
    CacheModule.registerAsync({
      isGlobal: true,
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => {
        const url = config.getOrThrow<string>('REDIS_URL');
        const keyv = new Keyv({
          store: createKeyv(url),
          namespace: 'devcraftly', // becomes the Redis key prefix
          ttl: 30_000, // default TTL in milliseconds
        });

        keyv.on('error', (err) => {
          // Never let a transient Redis error crash the process.
          console.error('Redis cache error', err);
        });

        return { stores: [keyv] };
      },
    }),
  ],
})
export class AppCacheModule {}

With isGlobal: true, the CACHE_MANAGER provider is available everywhere without re-importing the module. The keyv.on('error', ...) handler is important: a Redis blip should degrade you to cache misses, not unhandled rejections.

Using the shared cache in a service

Inject CACHE_MANAGER and the API is identical to the in-memory store — that is the whole point. Here a service caches an expensive lookup; the cached value is now visible to every instance.

// src/products/products.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';

@Injectable()
export class ProductsService {
  constructor(@Inject(CACHE_MANAGER) private readonly cache: Cache) {}

  async getProduct(id: string) {
    const key = `product:${id}`;
    const cached = await this.cache.get<{ id: string; name: string }>(key);
    if (cached) {
      return cached;
    }

    const product = await this.loadFromDatabase(id);
    // Per-call TTL (ms) overrides the module default.
    await this.cache.set(key, product, 60_000);
    return product;
  }

  async invalidate(id: string) {
    await this.cache.del(`product:${id}`);
  }

  private async loadFromDatabase(id: string) {
    await new Promise((r) => setTimeout(r, 200));
    return { id, name: 'Mirrorless Camera' };
  }
}

Inspecting Redis directly shows the namespaced key and the JSON-serialized value cache-manager wrote.

redis-cli KEYS 'devcraftly*'
redis-cli GET 'devcraftly:product:42'

Output:

1) "devcraftly:product:42"
{"value":{"id":"42","name":"Mirrorless Camera"},"expires":1749900000000}

Connection and serialization concerns

Keyv serializes values to JSON before writing to Redis, so anything you cache must be JSON-safe. This is the most common source of surprises when migrating from the in-memory store, which kept live object references.

ConcernWhat to know
SerializationValues are JSON-stringified. Date, Map, Set, BigInt, and class instances lose their type — a Date returns as a string.
Functions / circular refsCannot be serialized; strip them before caching or cache a plain DTO.
TTL unitscache-manager v5 TTLs are in milliseconds, unlike v4 which used seconds.
Key prefixThe Keyv namespace is prepended to every key, isolating apps and environments.
Connection stringredis://, rediss:// (TLS), and redis://user:pass@host:6379/0 are all accepted by @keyv/redis.
Failure modeA handled error event degrades to misses; wrap reads so a down Redis never breaks the request.

Gotcha: Because values round-trip through JSON, a cached entity’s Date fields come back as ISO strings. Re-hydrate them (new Date(value.createdAt)) after a cache hit, or cache a shape that has no Date fields, to avoid subtle type bugs downstream.

Best practices

  • Use registerAsync with ConfigService so the Redis URL and TLS settings come from environment variables, never hardcoded source.
  • Set a namespace (key prefix) per application and environment so staging and production can safely share a Redis cluster.
  • Always attach a keyv.on('error', ...) handler so a transient Redis outage degrades to cache misses instead of crashing the process.
  • Remember TTLs are milliseconds in cache-manager v5; auditing old second-based values prevents accidentally caching things for far too long.
  • Only cache JSON-serializable data, and re-hydrate Date/Map/class instances after a hit — do not assume reference identity survives the round trip.
  • Pair short TTLs with explicit del() invalidation on writes so every instance sees fresh data immediately after a mutation.
  • Enable a Redis maxmemory policy such as allkeys-lru so the cache evicts gracefully under pressure rather than refusing writes.
Last updated June 14, 2026
Was this helpful?