Skip to content
NestJS ns caching 4 min read

Caching Overview

Some operations are simply expensive: a database aggregation, a third-party API call, or a heavy computed report. Recomputing them on every request wastes CPU, money, and latency when the underlying data rarely changes. Caching stores the result of such work so subsequent requests can return it instantly. NestJS provides a unified caching layer through the @nestjs/cache-manager package, which wraps the popular cache-manager library and lets you swap between an in-memory store and external backends like Redis without touching your business logic.

Installing the package

The cache integration ships as a separate package alongside its cache-manager peer dependency. Install both from npm.

npm install @nestjs/cache-manager cache-manager

For modern NestJS (10/11) the cache-manager v5+ API is used, which is fully Promise-based. No extra store package is required for in-memory caching, which is the default.

Registering CacheModule

Caching is configured once by importing CacheModule into your root AppModule. The most useful option is isGlobal: true, which exports the cache provider application-wide so feature modules do not have to re-import CacheModule.

// src/app.module.ts
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';

@Module({
  imports: [
    CacheModule.register({
      isGlobal: true,
      ttl: 30_000, // milliseconds
      max: 100, // maximum number of items in memory
    }),
  ],
})
export class AppModule {}

The most common register options are summarized below.

OptionTypeDefaultEffect
isGlobalbooleanfalseRegisters the cache provider globally so feature modules need not re-import CacheModule
ttlnumber0 (no expiry)Default time-to-live for entries, in milliseconds
maxnumber100Maximum number of items held by the in-memory store before eviction
storestring | Storein-memoryThe backing store; defaults to memory, can be swapped for Redis and others

In cache-manager v5 and the matching @nestjs/cache-manager, ttl is expressed in milliseconds, not seconds. Older guides using seconds will silently expire entries far sooner than you expect.

Reading and writing the cache

To interact with the cache imperatively, inject the CACHE_MANAGER token. It resolves to a Cache instance whose get, set, and del methods are all asynchronous. This is ideal when you want fine-grained control, such as caching the result of an expensive lookup only on a miss.

// 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 getReport(id: string): Promise<Report> {
    const key = `report:${id}`;

    const cached = await this.cache.get<Report>(key);
    if (cached) {
      return cached;
    }

    const report = await this.computeExpensiveReport(id); // slow!
    await this.cache.set(key, report, 60_000); // override TTL: 60s
    return report;
  }

  private async computeExpensiveReport(id: string): Promise<Report> {
    // imagine a heavy DB aggregation here
    return { id, total: 42, generatedAt: new Date().toISOString() };
  }
}

interface Report {
  id: string;
  total: number;
  generatedAt: string;
}

The first call to getReport performs the slow computation and stores the result; every call within the next 60 seconds returns the cached value almost instantly.

Output:

GET /products/abc/report  -> 312ms   (cache miss, computed)
GET /products/abc/report  -> 2ms     (cache hit)
GET /products/abc/report  -> 1ms     (cache hit)
# after 60s the entry expires
GET /products/abc/report  -> 298ms   (cache miss, recomputed)

How TTL and eviction work

Each entry carries a time-to-live. When you call set without a third argument, the module’s default ttl applies; pass an explicit number to override it per key. A ttl of 0 means the entry never expires on its own. The in-memory store is also bounded by max: once that many items exist, the store evicts the oldest entries to make room, so memory cannot grow unbounded.

Because the default store lives inside the Node.js process, it is fast but local. In a multi-instance deployment each replica keeps its own copy, and a restart clears everything. For shared, durable caching across instances you switch the store to Redis, which is covered on a dedicated page.

Best Practices

  • Treat the cache as a performance optimization, never a source of truth — always be able to recompute the value from the underlying data.
  • Use descriptive, namespaced keys such as user:42:profile so entries are easy to target and invalidate.
  • Set a sensible TTL on every entry; an unbounded cache eventually serves stale data or exhausts memory.
  • Remember that ttl is in milliseconds in modern cache-manager — store 30_000, not 30, for 30 seconds.
  • Invalidate or update cached entries with del/set when the source data changes to avoid serving stale results.
  • Switch from the in-memory store to Redis before scaling beyond a single instance, so all replicas share one cache.
  • Only cache idempotent, read-heavy operations; caching responses that depend on the authenticated user or mutate state leads to subtle bugs.
Last updated June 14, 2026
Was this helpful?