January 10, 2025

NestJS Bull Queue Visualization Guide

CorbinCorbin

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 percentage
  • delay - Delay timestamp
  • timestamp - Creation time
  • attemptsMade - Retry count
  • failedReason - Error message if failed
  • stacktrace - Error stack if failed
  • returnvalue - 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:

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

  1. Processor never returns
@Process()
async handle(job: Job) {
  while (true) {
    // Infinite loop - job never completes
  }
}
  1. 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:

  1. Monitor bull:email:*
  2. Filter to failed jobs
  3. Select all, delete or inspect
  4. 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.

Ready for Download

Try Redimo Today

Pattern Monitor, CRUD operations, SSH Tunneling.
Everything you need to manage Redis at light speed.

macOS & Windows