NestJS and Redis in 2026: Caching, Sessions and Interview Questions
Learn how to implement Redis caching and session management in NestJS. Covers @nestjs/cache-manager setup, cache strategies, session persistence, and common interview questions on Node.js caching.

NestJS Redis integration transforms application performance by replacing database calls with in-memory lookups. The combination of @nestjs/cache-manager for caching and express-session with connect-redis for session persistence covers the two most common Redis use cases in backend applications.
Redis caching in NestJS reduces database load by storing frequently accessed data in memory. Session management with Redis enables horizontal scaling by sharing session state across multiple instances.
Setting Up @nestjs/cache-manager with Redis
The cache-manager module moved to a separate package in NestJS 10. Installing it requires both the NestJS wrapper and the underlying cache store. The @keyv/redis adapter is the recommended approach for Redis integration with cache-manager v5.
# Install required packages
npm install @nestjs/cache-manager cache-manager @keyv/redisThe module registration configures the Redis connection and default TTL. The isGlobal option makes the cache available across all modules without re-importing.
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import KeyvRedis from '@keyv/redis';
@Module({
imports: [
CacheModule.registerAsync({
isGlobal: true,
useFactory: () => ({
stores: [
new KeyvRedis(process.env.REDIS_URL || 'redis://localhost:6379'),
],
ttl: 60000, // Default TTL in milliseconds
}),
}),
],
})
export class AppModule {}The stores array accepts multiple cache stores for multi-tier caching. Production environments typically use Redis as the primary store, with an optional in-memory fallback for development.
Cache Injection and Service-Level Caching
The CACHE_MANAGER token provides direct access to the cache for service-level operations. This approach offers more control than automatic HTTP caching when business logic dictates cache invalidation.
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
import { UsersRepository } from './users.repository';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@Inject(CACHE_MANAGER) private cache: Cache,
private usersRepository: UsersRepository,
) {}
async findById(id: string): Promise<User | null> {
// Check cache first
const cacheKey = `user:${id}`;
const cached = await this.cache.get<User>(cacheKey);
if (cached) {
return cached;
}
// Cache miss: fetch from database
const user = await this.usersRepository.findById(id);
if (user) {
await this.cache.set(cacheKey, user, 300000); // 5 minutes TTL
}
return user;
}
async update(id: string, data: Partial<User>): Promise<User> {
const user = await this.usersRepository.update(id, data);
// Invalidate cache on update
await this.cache.del(`user:${id}`);
return user;
}
}The cache key pattern user:{id} enables targeted invalidation. More complex patterns like user:* require Redis-specific commands through the underlying client.
CacheInterceptor for Automatic HTTP Response Caching
The built-in CacheInterceptor caches HTTP GET responses automatically. Applying it at the controller level caches all GET endpoints, while method-level application provides granular control.
import { Controller, Get, UseInterceptors, Param } from '@nestjs/common';
import { CacheInterceptor, CacheTTL, CacheKey } from '@nestjs/cache-manager';
import { ProductsService } from './products.service';
@Controller('products')
@UseInterceptors(CacheInterceptor)
export class ProductsController {
constructor(private productsService: ProductsService) {}
@Get()
@CacheTTL(120000) // Override default TTL: 2 minutes
async findAll() {
return this.productsService.findAll();
}
@Get(':id')
@CacheKey('product-detail') // Custom cache key prefix
async findOne(@Param('id') id: string) {
return this.productsService.findById(id);
}
}The interceptor generates cache keys from the request URL by default. Custom @CacheKey() decorators override this behavior for routes where query parameters should not affect caching. For more on NestJS interceptors and guards, these concepts appear frequently in technical interviews.
The default cache key uses the full URL including query parameters. Two requests to /products?page=1 and /products?page=2 create separate cache entries. Override with @CacheKey() when pagination should not create duplicate cached responses.
Session Management with Redis and express-session
NestJS runs on Express by default, making express-session the standard choice for session management. The connect-redis adapter stores session data in Redis instead of memory, enabling session persistence across server restarts.
# Install session packages
npm install express-session connect-redis redis
npm install -D @types/express-sessionSession configuration happens in main.ts before the application starts. The Redis client connects independently from the cache-manager setup.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as session from 'express-session';
import { createClient } from 'redis';
import RedisStore from 'connect-redis';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Initialize Redis client
const redisClient = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379',
});
await redisClient.connect();
// Configure session middleware
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET || 'change-this-in-production',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
}),
);
await app.listen(3000);
}
bootstrap();The saveUninitialized: false option prevents empty sessions from being stored, reducing Redis memory usage. The secure: true cookie setting requires HTTPS in production.
Ready to ace your Node.js / NestJS interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Accessing Sessions in Controllers and Guards
Session data is available through the request object. A typed session interface improves developer experience and catches errors at compile time.
export interface SessionData {
userId?: string;
email?: string;
roles?: string[];
loginAt?: Date;
}
declare module 'express-session' {
interface SessionData {
userId?: string;
email?: string;
roles?: string[];
loginAt?: Date;
}
}The extended session type makes session properties available with autocompletion.
import { Controller, Post, Body, Req, HttpCode } from '@nestjs/common';
import { Request } from 'express';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
@HttpCode(200)
async login(@Body() loginDto: LoginDto, @Req() req: Request) {
const user = await this.authService.validateUser(
loginDto.email,
loginDto.password,
);
// Store user data in session
req.session.userId = user.id;
req.session.email = user.email;
req.session.roles = user.roles;
req.session.loginAt = new Date();
return { message: 'Logged in successfully' };
}
@Post('logout')
@HttpCode(200)
async logout(@Req() req: Request) {
return new Promise((resolve, reject) => {
req.session.destroy((err) => {
if (err) reject(err);
resolve({ message: 'Logged out successfully' });
});
});
}
}A guard protects routes by checking session validity. This pattern integrates cleanly with NestJS's authorization and RBAC system.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';
@Injectable()
export class SessionGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
return !!request.session?.userId;
}
}Cache Invalidation Strategies for NestJS Applications
Cache invalidation is the harder problem in caching. Three strategies apply to different scenarios:
Time-based expiration (TTL) works for data that changes predictably or where slight staleness is acceptable. Product catalogs, configuration values, and public content fit this pattern.
Event-based invalidation clears cache entries when the underlying data changes. This requires explicit cache.del() calls in update and delete operations.
Pattern-based invalidation removes multiple related keys. Redis supports pattern deletion through SCAN and DEL commands, but cache-manager abstracts this away.
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
@Injectable()
export class CacheInvalidationService {
constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}
// Single key invalidation
async invalidateUser(userId: string): Promise<void> {
await this.cache.del(`user:${userId}`);
}
// Multiple related keys
async invalidateUserRelated(userId: string): Promise<void> {
const keys = [
`user:${userId}`,
`user:${userId}:profile`,
`user:${userId}:preferences`,
];
await Promise.all(keys.map((key) => this.cache.del(key)));
}
// Clear entire cache (use sparingly)
async clearAll(): Promise<void> {
await this.cache.reset();
}
}The reset() method clears all cached data, useful during deployments or data migrations but dangerous in production if called accidentally.
Redis Connection Pooling and Cluster Support
Production deployments require connection pooling to handle concurrent requests efficiently. The ioredis library provides cluster support and connection pooling out of the box.
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Redis, { Cluster } from 'ioredis';
@Injectable()
export class RedisConfig {
constructor(private configService: ConfigService) {}
createClient(): Redis | Cluster {
const clusterNodes = this.configService.get<string>('REDIS_CLUSTER_NODES');
if (clusterNodes) {
// Cluster mode
const nodes = clusterNodes.split(',').map((node) => {
const [host, port] = node.split(':');
return { host, port: parseInt(port, 10) };
});
return new Redis.Cluster(nodes, {
redisOptions: {
password: this.configService.get('REDIS_PASSWORD'),
},
});
}
// Standalone mode
return new Redis({
host: this.configService.get('REDIS_HOST', 'localhost'),
port: this.configService.get('REDIS_PORT', 6379),
password: this.configService.get('REDIS_PASSWORD'),
maxRetriesPerRequest: 3,
});
}
}For applications using TypeORM or Prisma, the database and cache connections should be configured separately with their own connection pools.
NestJS Redis Interview Questions
Technical interviews on Node.js caching cover both conceptual understanding and practical implementation. These questions appear frequently:
Why use Redis instead of in-memory caching?
In-memory caching does not persist across restarts and cannot be shared across multiple instances. Redis provides persistence, replication, and shared state for horizontally scaled applications. The tradeoff is network latency compared to local memory access.
How does cache-manager differ from direct Redis usage?
The cache-manager library provides a unified API across different cache stores. Switching from in-memory to Redis requires changing the store configuration without modifying service code. Direct Redis usage offers more Redis-specific features like pub/sub, sorted sets, and Lua scripting.
What is cache stampede and how do you prevent it?
Cache stampede occurs when a popular cache entry expires and many concurrent requests hit the database simultaneously. Prevention strategies include:
- Lock-based refresh: One request refreshes the cache while others wait
- Probabilistic early expiration: Randomly refresh before TTL expires
- Background refresh: A separate process refreshes expiring entries
When should you use CacheInterceptor vs. service-level caching?
CacheInterceptor suits stateless GET endpoints where the response depends only on the URL. Service-level caching handles scenarios where cache invalidation must occur on data changes or where non-GET operations need caching.
How do you handle session fixation attacks with Redis sessions?
Regenerate the session ID after authentication. Call req.session.regenerate() before storing user credentials to prevent attackers from using pre-authentication session IDs.
// Secure login with session regeneration
async login(@Body() loginDto: LoginDto, @Req() req: Request) {
const user = await this.authService.validateUser(
loginDto.email,
loginDto.password,
);
return new Promise((resolve, reject) => {
req.session.regenerate((err) => {
if (err) reject(err);
req.session.userId = user.id;
req.session.email = user.email;
resolve({ message: 'Logged in successfully' });
});
});
}Senior candidates explain the tradeoffs between caching strategies. A junior might say "caching improves performance." A senior explains when caching adds complexity without benefit, such as highly personalized or rapidly changing data.
Performance Monitoring and Redis Metrics
Production applications need visibility into cache performance. Key metrics include:
- Hit rate: Percentage of requests served from cache. Below 80% suggests TTL or key strategy issues.
- Memory usage: Redis
INFO memorycommand shows current and peak memory. - Connection count: Too many connections indicate missing pooling.
- Eviction count: Non-zero evictions mean the cache is full and dropping entries.
import { Injectable } from '@nestjs/common';
import Redis from 'ioredis';
@Injectable()
export class RedisHealthService {
constructor(private redis: Redis) {}
async getMetrics() {
const info = await this.redis.info('stats');
const memory = await this.redis.info('memory');
const stats = this.parseInfo(info);
const mem = this.parseInfo(memory);
const hits = parseInt(stats.keyspace_hits, 10);
const misses = parseInt(stats.keyspace_misses, 10);
const hitRate = hits / (hits + misses) || 0;
return {
hitRate: (hitRate * 100).toFixed(2) + '%',
usedMemory: mem.used_memory_human,
connectedClients: stats.connected_clients,
evictedKeys: stats.evicted_keys,
};
}
private parseInfo(info: string): Record<string, string> {
return info.split('\n').reduce((acc, line) => {
const [key, value] = line.split(':');
if (key && value) acc[key.trim()] = value.trim();
return acc;
}, {} as Record<string, string>);
}
}Expose these metrics through a health check endpoint or integrate with Prometheus for production monitoring.
Redis Caching and Sessions in Production NestJS Deployments
Deploying NestJS with Redis requires attention to several operational concerns:
- Connection string management: Store Redis URLs in environment variables, not code. Use secrets management for passwords.
- Graceful shutdown: Close Redis connections during application shutdown to prevent connection leaks.
- Retry logic: Configure exponential backoff for Redis connection failures.
- Separate Redis instances: Consider separate Redis instances for cache and sessions. Cache eviction should not affect session data.
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import Redis from 'ioredis';
@Injectable()
export class RedisCleanup implements OnModuleDestroy {
constructor(private redis: Redis) {}
async onModuleDestroy() {
await this.redis.quit();
}
}For microservices architectures, Redis also serves as a message broker through NestJS's built-in transport layer, unifying caching, sessions, and inter-service communication.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Can you spot the bug in Node.js / NestJS?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on September 10, 2026
Tags
Share
Related articles

NestJS and MongoDB in 2026: Mongoose, Aggregations and Interview Questions
Master NestJS with MongoDB and Mongoose 9. Learn schema design, aggregation pipelines, and prepare for technical interviews with practical examples.

NestJS Interview: Guards, Interceptors and Modular Architecture
Common NestJS interview questions on Guards, Interceptors and modular architecture, with concrete TypeScript code examples and technical explanations.

NestJS + Prisma: The Modern Backend Stack for Node.js
Complete guide to building a modern backend API with NestJS and Prisma. Setup, models, services, transactions and best practices explained.