NestJS Bull Queue Visualization Guide
NestJS with @nestjs/bull (or @nestjs/bullmq) makes background job processing elegant. Decorators, dependency injection, modules - it's clean.
Until jobs start failing and you can't figure out why. The decorator abstraction that makes coding easy makes debugging hard.
What NestJS Bull Creates in Redis
When you define a queue:
@Module({
imports: [
BullModule.registerQueue({
name: 'email',
}),
],
})
export class EmailModule {}
NestJS Bull creates these Redis keys:
bull:email:id # Job ID counter
bull:email:waiting # Jobs waiting to be processed
bull:email:active # Jobs currently processing
bull:email:completed # Completed job IDs
bull:email:failed # Failed job IDs
bull:email:delayed # Scheduled/delayed jobs
bull:email:paused # Queue pause state
bull:email:meta # Queue metadata
bull:email:<job-id> # Individual job data
Each queue you register gets this full structure.
Standard Debugging Options
Option 1: Bull Board
import { BullBoardModule } from '@bull-board/nestjs';
import { ExpressAdapter } from '@bull-board/express';
@Module({
imports: [
BullBoardModule.forRoot({
route: '/queues',
adapter: ExpressAdapter,
}),
BullBoardModule.forFeature({
name: 'email',
adapter: BullAdapter,
}),
],
})
Gives you a web UI at /queues. Good for basic monitoring.
Limitations:
- Another endpoint to secure
- Can't query jobs by custom criteria
- No bulk operations
- Doesn't show raw Redis data
Option 2: Bull CLI
npx bull-cli
Connects to Redis, shows queue stats. Limited interactivity.
Option 3: Logging
@Processor('email')
export class EmailProcessor {
private readonly logger = new Logger(EmailProcessor.name);
@Process()
async handleEmail(job: Job<EmailJobData>) {
this.logger.log(`Processing job ${job.id}`);
try {
// ... process
this.logger.log(`Completed job ${job.id}`);
} catch (e) {
this.logger.error(`Failed job ${job.id}`, e.stack);
throw e;
}
}
}
Good for tracking flow, not great for investigating state.
Going Deeper: Redis Level
When standard tools don't answer your questions, look at Redis directly.
Find All Your Queues
redis-cli KEYS "bull:*:id"
Each result is a queue. bull:email:id, bull:notifications:id, etc.
Queue Health Check
# How many jobs in each state?
redis-cli LLEN bull:email:waiting
redis-cli LLEN bull:email:active
redis-cli SCARD bull:email:completed
redis-cli SCARD bull:email:failed
redis-cli ZCARD bull:email:delayed
Inspect a Specific Job
redis-cli HGETALL bull:email:123
Returns the job hash:
data- Your job payload (JSON)opts- Job options (attempts, delay, etc.)progress- Progress percentagedelay- Delay timestamptimestamp- Creation timeattemptsMade- Retry countfailedReason- Error message if failedstacktrace- Error stack if failedreturnvalue- Result if completed
Pattern-Based Investigation
Monitor All Bull Queues
In Redimo, create a Pattern Monitor:
Pattern: bull:*
See all queues, all jobs, all states at once. The tree view groups by queue name:
bull:
email:
waiting (12 items)
active (3 items)
failed (2 items)
1, 2, 3... (job hashes)
notifications:
waiting (5 items)
...
Monitor Specific Queue
Pattern: bull:email:*
Focus on one queue. See:
- How many waiting vs active
- Failed jobs with error details
- Delayed jobs and their scheduled times
Click any job hash to expand and see full data.
Common NestJS Bull Issues
Issue 1: Jobs Not Processing
Jobs pile up in waiting. Processor isn't running.
Check 1: Is the processor module imported?
// app.module.ts
@Module({
imports: [
EmailModule, // Must be imported!
],
})
export class AppModule {}
Check 2: Is the processor decorated correctly?
@Processor('email') // Queue name must match
export class EmailProcessor {
@Process() // Default job handler
// or @Process('specific-job-name')
async handle(job: Job) {}
}
Check 3: Redis keys
Look at bull:email:waiting. Are jobs actually there? Are they malformed?
Issue 2: Jobs Stuck in Active
Jobs move to active but never complete.
Possible causes:
- Processor throws but error isn't caught
@Process()
async handle(job: Job) {
await someAsyncThing(); // Throws
// Job stays active forever
}
Bull doesn't know it failed. Add error handling or let it throw properly.
- Processor never returns
@Process()
async handle(job: Job) {
while (true) {
// Infinite loop - job never completes
}
}
- Worker crashed mid-job
Check bull:email:active. Old jobs here? Worker died. They'll be retried based on your stalledInterval setting.
Issue 3: High Memory Usage
Bull queues growing. Redis memory increasing.
Check completed/failed retention:
BullModule.registerQueue({
name: 'email',
defaultJobOptions: {
removeOnComplete: 100, // Keep last 100
removeOnFail: 50, // Keep last 50
},
}),
Without this, completed and failed jobs stay forever.
In Redimo:
- Monitor
bull:email:completed- how big is this set? - Check individual job hashes - are they huge?
Issue 4: Jobs Processed Multiple Times
Same job running on multiple workers.
Cause: Stalled jobs. If a worker doesn't report progress fast enough, Bull assumes it died and re-queues the job.
BullModule.registerQueue({
name: 'email',
settings: {
stalledInterval: 30000, // Check every 30s
maxStalledCount: 2, // Allow 2 stalls before failing
},
}),
In processor:
@Process()
async handle(job: Job) {
for (const item of items) {
await processItem(item);
await job.progress(50); // Report progress to prevent stall
}
}
Issue 5: Delayed Jobs Not Running
Jobs scheduled for the future never execute.
Check bull:email:delayed:
This is a sorted set. Score = timestamp when job should run.
redis-cli ZRANGE bull:email:delayed 0 -1 WITHSCORES
Scores in the past? Something's wrong with the delay processing.
Possible cause: No workers running. Delayed jobs need a worker to poll and move them to waiting.
Advanced: Custom Job Events
NestJS Bull exposes job lifecycle events:
@Processor('email')
export class EmailProcessor {
@OnQueueActive()
onActive(job: Job) {
console.log(`Job ${job.id} started`);
}
@OnQueueCompleted()
onCompleted(job: Job, result: any) {
console.log(`Job ${job.id} completed:`, result);
}
@OnQueueFailed()
onFailed(job: Job, error: Error) {
console.log(`Job ${job.id} failed:`, error.message);
}
@OnQueueStalled()
onStalled(job: Job) {
console.log(`Job ${job.id} stalled`);
}
}
Log these events to catch issues early.
Bulk Operations
Need to clear failed jobs? Retry them all?
NestJS way:
@Injectable()
export class QueueService {
constructor(@InjectQueue('email') private queue: Queue) {}
async clearFailed() {
const failed = await this.queue.getFailed();
await Promise.all(failed.map(job => job.remove()));
}
async retryFailed() {
const failed = await this.queue.getFailed();
await Promise.all(failed.map(job => job.retry()));
}
}
Redimo way:
- Monitor
bull:email:* - Filter to failed jobs
- Select all, delete or inspect
- No code changes needed
Production Checklist
BullModule.registerQueue({
name: 'email',
defaultJobOptions: {
attempts: 3, // Retry 3 times
backoff: {
type: 'exponential',
delay: 1000, // 1s, 2s, 4s
},
removeOnComplete: 100, // Don't keep forever
removeOnFail: 500, // Keep more failures for debugging
},
settings: {
stalledInterval: 30000,
maxStalledCount: 3,
},
}),
Monitor:
- Queue lengths (waiting + active)
- Failed job rate
- Memory usage of bull:* keys
- Job processing duration
Quick Reference
| NestJS Concept | Redis Key | What to Check |
|---|---|---|
| Queue | bull:<name>:* |
Overall queue health |
| Waiting jobs | bull:<name>:waiting |
Backlog size |
| Active jobs | bull:<name>:active |
Stuck jobs |
| Failed jobs | bull:<name>:failed |
Error patterns |
| Job data | bull:<name>:<id> |
Payload, error, state |
| Delayed | bull:<name>:delayed |
Scheduled times |
NestJS Bull makes job processing elegant. When the abstraction hides problems, Redis shows you the truth.
Download Redimo and see your NestJS Bull queues clearly.