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.
| Option | Type | Default | Effect |
|---|---|---|---|
isGlobal | boolean | false | Registers the cache provider globally so feature modules need not re-import CacheModule |
ttl | number | 0 (no expiry) | Default time-to-live for entries, in milliseconds |
max | number | 100 | Maximum number of items held by the in-memory store before eviction |
store | string | Store | in-memory | The backing store; defaults to memory, can be swapped for Redis and others |
In
cache-managerv5 and the matching@nestjs/cache-manager,ttlis 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:profileso 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
ttlis in milliseconds in moderncache-manager— store30_000, not30, for 30 seconds. - Invalidate or update cached entries with
del/setwhen 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.