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 and Redis caching architecture diagram showing session management and cache layers

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.

app.module.tstypescript
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.

users.service.tstypescript
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.

products.controller.tstypescript
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.

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 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.

main.tstypescript
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.

session.interface.tstypescript
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.

auth.controller.tstypescript
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.

session.guard.tstypescript
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.

cache-invalidation.service.tstypescript
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.

redis.config.tstypescript
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.

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.
redis-health.service.tstypescript
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.
graceful-shutdown.tstypescript
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.

Daily challenge

Can you spot the bug in Node.js / NestJS?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on September 10, 2026

Tags

#nestjs
#redis
#caching
#nodejs
#sessions
#interview

Share

Related articles