# 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. - Published: 2026-09-10 - Updated: 2026-09-10 - Author: Anthony Fillion-Maillet - Tags: nestjs, redis, caching, nodejs, sessions, interview - Reading time: 12 min --- 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. > **Key Takeaway** > > 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. ```bash # Install required packages npm install @nestjs/cache-manager cache-manager @keyv/redis ``` The module registration configures the Redis connection and default TTL. The `isGlobal` option makes the cache available across all modules without re-importing. ```typescript // app.module.ts 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. ```typescript // users.service.ts 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 { // Check cache first const cacheKey = `user:${id}`; const cached = await this.cache.get(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): Promise { 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. ```typescript // products.controller.ts 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](/en/technologies/node-nestjs/interview-questions/middleware-interceptors), these concepts appear frequently in technical interviews. > **Cache Key Collisions** > > 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](https://github.com/expressjs/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. ```bash # Install session packages npm install express-session connect-redis redis npm install -D @types/express-session ``` Session configuration happens in `main.ts` before the application starts. The Redis client connects independently from the cache-manager setup. ```typescript // main.ts 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. ## 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. ```typescript // session.interface.ts 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. ```typescript // auth.controller.ts 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](/en/technologies/node-nestjs/interview-questions/authorization-rbac) system. ```typescript // session.guard.ts 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(); 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. ```typescript // cache-invalidation.service.ts 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 { await this.cache.del(`user:${userId}`); } // Multiple related keys async invalidateUserRelated(userId: string): Promise { 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 { 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](https://github.com/redis/ioredis) library provides cluster support and connection pooling out of the box. ```typescript // redis.config.ts 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('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](/en/blog/node-nestjs/nestjs-typeorm-migrations-relations-best-practices) 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. ```typescript // 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' }); }); }); } ``` > **Interview Insight** > > 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 memory` command 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. ```typescript // redis-health.service.ts 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 { return info.split('\n').reduce((acc, line) => { const [key, value] = line.split(':'); if (key && value) acc[key.trim()] = value.trim(); return acc; }, {} as Record); } } ``` 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. ```typescript // graceful-shutdown.ts 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](/en/blog/node-nestjs/nestjs-microservices-grpc-architecture), Redis also serves as a message broker through NestJS's built-in transport layer, unifying caching, sessions, and inter-service communication. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/node-nestjs/nestjs-redis-caching-sessions-interview-questions