Vấn đề gặp phải
Khi deploy NestJS application lên production sử dụng BullMQ với AWS ElastiCache (Redis), chúng ta thường gặp lỗi job bị stall dù Redis vẫn đang hoạt động bình thường:
Error: Job stalled
attempts made: 1
timeout of 30000ms exceeded
Job queue vẫn nhận message, nhưng worker bị disconnect sau một thời gian idle.
Nguyên nhân
AWS ElastiCache có cấu hình TCP keepalive timeout mặc định là 350 giây. Nếu connection không có activity trong khoảng thời gian đó, ElastiCache sẽ đóng connection từ phía server.
BullMQ sử dụng long-lived Redis connections để lắng nghe các blocking operations (BRPOP, BLMOVE). Nếu không có job nào trong queue, connection sẽ idle và bị ElastiCache đóng mà client không hay biết.
Khi worker cố gắng dùng connection đã bị đóng này, nó sẽ throw timeout error.
Cấu hình BullMQ đúng cách
Có hai cách khắc phục:
Enable TCP Keepalive
import { BullModule } from '@nestjs/bullmq';
BullModule.forRoot({
connection: {
host: process.env.REDIS_HOST,
port: 6379,
keepAlive: 30000, // gửi keepalive mỗi 30s
connectTimeout: 10000,
maxRetriesPerRequest: 3,
},
});
Sử dụng ioredis với explicit options
import Redis from 'ioredis';
const redisConnection = new Redis({
host: process.env.REDIS_HOST,
port: 6379,
keepAlive: 30000,
enableOfflineQueue: false,
retryStrategy: (times) => Math.min(times * 50, 2000),
reconnectOnError: (err) => {
const targetError = 'READONLY';
return err.message.includes(targetError);
},
});
BullModule.forRoot({ connection: redisConnection });
Tăng timeout ở ElastiCache
Trong ElastiCache parameter group, tăng tcp-keepalive lên 60 và timeout về 0 (disable):
tcp-keepalive = 60
timeout = 0
Kết luận
Vấn đề không phải ở ElastiCache hay BullMQ — mà ở sự không khớp giữa TCP idle timeout của AWS và connection lifecycle của BullMQ. Giải pháp tốt nhất là:
- Set
keepAlive: 30000trong ioredis config - Tăng
connectTimeoutlên 10-15s - Implement proper retry strategy
Với cấu hình này, worker sẽ chủ động gửi TCP keepalive packets trước khi ElastiCache đóng connection.