When this becomes a real production problem
In production, the failure mode is usually not "Redis is fully down". It is more often stuck commands, slow queues, or reconnect loops after a short network issue or an ElastiCache failover. That is where ioredis defaults tend to be too weak.
Common causes:
- Temporary network blips
- Redis server restart (maintenance/update)
- Cloud provider TCP timeout
- Load balancer connection draining
NestJS with ioredis has retry logic by default, but it is not optimized for production environments.
Retry strategy
import Redis from 'ioredis';
import { Logger } from '@nestjs/common';
const logger = new Logger('Redis');
export function createRedisConnection(): Redis {
return new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
// Reconnect with exponential backoff
retryStrategy: (times: number) => {
if (times > 10) {
logger.error('Redis: max retries reached, giving up');
return null; // stop retrying
}
const delay = Math.min(times * 100, 3000);
logger.warn(`Redis: retry #${times} in ${delay}ms`);
return delay;
},
// Reconnect on READONLY error (ElastiCache failover)
reconnectOnError: (err: Error) => {
logger.error('Redis connection error:', err.message);
return err.message.includes('READONLY');
},
// Connection options
keepAlive: 30000,
connectTimeout: 10000,
commandTimeout: 5000,
enableOfflineQueue: true,
maxRetriesPerRequest: 3,
lazyConnect: false,
});
}
Health check
import { Injectable } from '@nestjs/common';
import {
HealthIndicator,
HealthIndicatorResult,
} from '@nestjs/terminus';
import Redis from 'ioredis';
@Injectable()
export class RedisHealthIndicator extends HealthIndicator {
constructor(private readonly redis: Redis) {
super();
}
async isHealthy(key: string): Promise<HealthIndicatorResult> {
try {
const ping = await this.redis.ping();
return this.getStatus(key, ping === 'PONG');
} catch (err) {
return this.getStatus(key, false, { error: err.message });
}
}
}
Monitoring and alerting
Listen to ioredis events to log and alert:
const redis = createRedisConnection();
redis.on('connect', () => logger.log('Redis connected'));
redis.on('ready', () => logger.log('Redis ready'));
redis.on('error', (err) => logger.error('Redis error', err));
redis.on('close', () => logger.warn('Redis connection closed'));
redis.on('reconnecting', (ms: number) =>
logger.warn(`Redis reconnecting in ${ms}ms`)
);
redis.on('end', () => logger.error('Redis connection ended'));
With this setup, the application automatically recovers from a Redis outage without needing a restart.