ZTechUniverse

Bull Queue with NestJS and Redis — Setup and Benefits

2025-02-18 · 3 min read

TL;DR — Queues decouple work from the request. Use Bull + Redis for email, notifications, heavy jobs. Benefits: absorb spikes, retries, scale workers. In NestJS, use @nestjs/bull and BullModule.


What Is a Queue?

A queue is a buffer between producers (who add jobs) and consumers (who process them). Instead of doing work during the HTTP request, you push a job and return immediately. A worker processes it in the background.

Benefits:

  • Absorb traffic spikes — 1000 signups at once? Queue 1000 emails. Process them in order.
  • Retries — Job fails? Retry automatically with backoff.
  • Scale workers — Run more workers to process faster.
  • Decoupling — API doesn't depend on email service being up. If email fails, the job stays in the queue.

When to Use Queues

  • Send emails — Welcome, password reset, notifications.
  • Heavy processing — Image resizing, PDF generation, report generation.
  • External API calls — Don't block the request while waiting.
  • Scheduled tasks — Cleanup, sync, reminders.

Setup Bull in NestJS with Redis

1. Install packages

Bash
npm install @nestjs/bull bull

2. Add Redis to your app

Bull uses Redis to store jobs. You need Redis running (local or cloud like Upstash, Redis Cloud).

3. Register the queue module

TypeScript
// app.module.ts import { BullModule } from "@nestjs/bull"; @Module({ imports: [ BullModule.forRoot({ redis: { host: process.env.REDIS_HOST || "localhost", port: parseInt(process.env.REDIS_PORT || "6379"), }, }), BullModule.registerQueue({ name: "email", }), ], }) export class AppModule {}

4. Create a processor (worker)

TypeScript
// email.processor.ts import { Process, Processor } from "@nestjs/bull"; import { Job } from "bull"; import { EmailService } from "./email.service"; @Processor("email") export class EmailProcessor { constructor(private readonly emailService: EmailService) {} @Process("welcome") async handleWelcome(job: Job<{ to: string; name: string }>) { const { to, name } = job.data; await this.emailService.sendWelcome(to, name); } @Process("password-reset") async handlePasswordReset(job: Job<{ to: string; token: string }>) { const { to, token } = job.data; await this.emailService.sendPasswordReset(to, token); } }

5. Add jobs from your service

TypeScript
// users.service.ts import { InjectQueue } from "@nestjs/bull"; import { Queue } from "bull"; @Injectable() export class UsersService { constructor( @InjectQueue("email") private emailQueue: Queue, private readonly userRepo: UserRepository ) {} async createUser(dto: CreateUserDto) { const user = await this.userRepo.create(dto); // Queue welcome email — don't wait await this.emailQueue.add("welcome", { to: user.email, name: user.name, }); return user; } }

Retries and Options

TypeScript
await this.emailQueue.add("welcome", data, { attempts: 3, backoff: { type: "exponential", delay: 1000, // 1s, 2s, 4s }, removeOnComplete: 100, });

Benefits Summary

BenefitHow
Non-blockingAPI returns quickly; work happens in background
RetriesAutomatic retries with backoff
Rate limitingProcess N jobs per second
PersistenceJobs survive restarts (Redis)
ScalabilityAdd more workers to process faster

For more backend patterns, see Backend Best Practices.