Skip to content

Failure Modes

How NestJS RedisX behaves when Redis fails, and how to configure graceful degradation.

Failure Types

FailureSymptomDuration
Network blipTimeout errorsSeconds
Redis restartConnection refusedMinutes
Redis crashConnection refusedMinutes to hours
Network partitionTimeouts, partial failuresVaries

Default Behaviors

PluginDefault on FailureRationale
CacheBypass (hit database)Availability over freshness
LocksThrow errorSafety over availability
Rate LimitAllow requestAvailability over protection
IdempotencyThrow errorSafety over availability
StreamsQueue locally, retryDurability

Configuring Fallback Behavior

Cache

typescript
new CachePlugin({
  fallback: 'bypass',  // Default: skip cache, hit DB
  // or
  fallback: 'throw',   // Fail the request
  // or
  fallback: (error) => {
    logger.error('Cache failed', error);
    return 'bypass';
  },
})

Locks

typescript
new LocksPlugin({
  fallback: 'throw',   // Default: fail if can't acquire
  // or
  fallback: 'allow',   // Proceed without lock (dangerous!)
})

// Per-operation override
@WithLock({
  key: 'resource:{0}',
  fallback: 'throw',  // Critical operation
})

Rate Limit

typescript
new RateLimitPlugin({
  fallback: 'allow',   // Default: allow if can't check
  // or
  fallback: 'deny',    // Block if can't verify
})

Idempotency

typescript
new IdempotencyPlugin({
  fallback: 'throw',   // Default: fail if can't check
  // or
  fallback: 'allow',   // Process anyway (risk of duplicates)
})

Circuit Breaker Pattern

Prevent cascading failures with circuit breaker:

typescript
new CachePlugin({
  circuitBreaker: {
    enabled: true,
    threshold: 5,        // Failures before opening
    timeout: 30000,      // Time before half-open
    resetTimeout: 5000,  // Time in half-open
  },
})

Retry Configuration

typescript
new RedisModule.forRoot({
  clients: {
    host: 'redis',
    retryStrategy: (times) => {
      if (times > 10) {
        return null; // Stop retrying
      }
      return Math.min(times * 100, 3000); // Exponential backoff
    },
  },
})

Health Checks

Detect failures early:

typescript
@Controller('health')
export class HealthController {
  constructor(private redis: RedisService) {}

  @Get('ready')
  async readiness() {
    try {
      await this.redis.ping();
      return { status: 'ok', redis: 'connected' };
    } catch (error) {
      return { status: 'degraded', redis: 'disconnected' };
    }
  }
}

Graceful Degradation Strategies

Cache: Serve Stale

typescript
@Cached({
  ttl: 300,
  staleWhileRevalidate: true,
  staleTime: 3600,  // Serve stale for 1 hour
  onError: 'stale', // Return stale on error
})
async getProduct(id: string) {
  return this.productService.fetch(id);
}

Locks: Time-bounded Operations

typescript
@WithLock({
  key: 'resource:{0}',
  ttl: 30000,
  waitTimeout: 5000,
  onLockFailed: 'skip',  // Skip if can't acquire in 5s
})
async nonCriticalOperation(id: string) {
  // Will skip rather than block forever
}

Rate Limit: Local Fallback

typescript
@Injectable()
export class ResilientRateLimitGuard implements CanActivate {
  private localCounts = new Map<string, number>();

  async canActivate(context: ExecutionContext): Promise<boolean> {
    try {
      return await this.redisRateLimit.check(/*...*/);
    } catch (error) {
      // Fall back to local rate limiting
      return this.localRateLimit(context);
    }
  }

  private localRateLimit(context: ExecutionContext): boolean {
    const key = this.getKey(context);
    const count = this.localCounts.get(key) || 0;
    if (count >= 100) return false;
    this.localCounts.set(key, count + 1);
    setTimeout(() => this.localCounts.delete(key), 60000);
    return true;
  }
}

Failure Recovery

Automatic Reconnection

typescript
// ioredis handles reconnection automatically
// Configure behavior:
{
  retryStrategy: (times) => Math.min(times, 10) * 1000,
  maxRetriesPerRequest: 3,
  reconnectOnError: (err) => {
    return err.message.includes('READONLY');
  },
}

Cache Warming After Recovery

typescript
@Injectable()
export class CacheRecoveryService {
  @OnEvent('redis.connected')
  async onRedisConnected() {
    // Warm critical caches
    await this.warmProductCache();
    await this.warmConfigCache();
  }

  private async warmProductCache() {
    const popular = await this.productRepo.findPopular(100);
    for (const product of popular) {
      await this.cache.set(`product:${product.id}`, product);
    }
  }
}

Monitoring Failures

yaml
# Redis connection status
redis_up

# Connection errors
rate(redis_connection_errors_total[5m])

# Circuit breaker state
redis_circuit_breaker_state{state="open"}

# Fallback activations
rate(redis_fallback_total[5m])

Next Steps

Released under the MIT License.