js

Build a Distributed Task Queue System with BullMQ, Redis, and TypeScript Tutorial

Learn to build scalable distributed task queues with BullMQ, Redis & TypeScript. Master job processing, error handling, scaling & monitoring for production apps.

Build a Distributed Task Queue System with BullMQ, Redis, and TypeScript Tutorial

I’ve been thinking a lot about how modern applications handle heavy workloads without crashing. When building systems that send emails, process media, or crunch data, we can’t afford to block users while these tasks run. That’s what brought me to distributed task queues - they let us offload work to background processes. Today, I’ll show you how to build one using BullMQ, Redis, and TypeScript. Stick around - this could change how you design your next project.

First, why use a queue? Imagine 10,000 users requesting image processing simultaneously. Without a queue, your server would drown. With BullMQ and Redis, we can manage this elegantly. Redis acts as the backbone, storing jobs and coordinating workers. BullMQ provides the tools to define, process, and monitor these jobs. TypeScript ensures we catch errors early with type safety.

Setting up is straightforward. Create a new project and install dependencies:

npm install bullmq redis @types/node tsx
npm install -D typescript @types/redis nodemon

Our tsconfig.json ensures strict type checking. We organize code into logical directories: queues for job definitions, workers for processing logic, and jobs for shared types. Here’s how we establish the Redis connection:

// src/config/redis.ts
import { Redis } from 'ioredis';

export const redisConnection = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  maxRetriesPerRequest: 3
});

redisConnection.on('error', (err) => {
  console.error('Redis error:', err);
});

Now, what makes a robust queue system? Let’s define our job types first. TypeScript interfaces prevent mismatched data:

// src/jobs/types.ts
export interface EmailJobData {
  to: string;
  subject: string;
  body: string;
}

export interface ImageJobData {
  url: string;
  width: number;
  height: number;
}

Creating a queue becomes simple with BullMQ. Notice how we attach event listeners for monitoring:

// src/queues/email-queue.ts
import { Queue } from 'bullmq';
import { redisConnection } from '../config/redis';
import { EmailJobData } from '../jobs/types';

export const emailQueue = new Queue<EmailJobData>('email', {
  connection: redisConnection,
  defaultJobOptions: {
    attempts: 3,
    backoff: { type: 'exponential', delay: 2000 }
  }
});

emailQueue.on('completed', job => {
  console.log(`Email sent to ${job.data.to}`);
});

Workers process jobs independently. Here’s an email worker with error handling:

// src/workers/email-worker.ts
import { Worker } from 'bullmq';
import { redisConnection } from '../config/redis';
import { EmailJobData } from '../jobs/types';

const worker = new Worker<EmailJobData>('email', async job => {
  const { to, subject, body } = job.data;
  // Simulate email sending
  if (!to.includes('@')) throw new Error('Invalid email');
  console.log(`Sending email to ${to}`);
}, { connection: redisConnection });

worker.on('failed', (job, err) => {
  console.error(`Email to ${job?.data.to} failed:`, err);
});

What happens when jobs fail? BullMQ’s retry system saves us. The exponential backoff means failed jobs wait longer before retrying - perfect for temporary outages. For permanent failures, we log them for investigation.

Scaling is where this shines. Spin up multiple workers across servers:

# Worker instance 1
tsx src/workers/email-worker.ts

# Worker instance 2
tsx src/workers/email-worker.ts

Redis coordinates everything. Workers compete for jobs, ensuring parallel processing. BullMQ’s dashboard gives real-time insights:

// src/monitoring/dashboard.ts
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ExpressAdapter } from '@bull-board/express';
import { emailQueue } from '../queues/email-queue';

const serverAdapter = new ExpressAdapter();

createBullBoard({
  queues: [new BullMQAdapter(emailQueue)],
  serverAdapter
});

serverAdapter.setBasePath('/admin/queues');
export default serverAdapter.getRouter();

Advanced features like scheduling come built-in. Delay critical emails during off-peak hours:

await emailQueue.add('low-priority-email', {
  to: 'user@example.com',
  subject: 'Weekly digest',
  body: '...'
}, { delay: 86_400_000 }); // 24 hours later

Prioritization ensures urgent tasks jump ahead. In healthcare apps, patient alerts might override marketing emails:

await emailQueue.add('high-priority', {
  to: 'doctor@hospital.org',
  subject: 'URGENT: Patient update'
}, { priority: 1 }); // Highest priority

What about rate limits? BullMQ handles that too. Limit third-party API calls to avoid bans:

const apiQueue = new Queue('external-api', {
  limiter: { max: 100, duration: 60_000 } // 100/minute
});

In production, separate Redis instances for queues and cache. Use connection pooling and monitor memory usage. Always set TTLs on jobs to prevent accumulation. Test failure scenarios - what happens when Redis disconnects? How do workers recover?

I’ve seen teams transform brittle systems into resilient architectures using these patterns. The separation of concerns lets frontend remain responsive while backend workers crunch data. Have you considered how queues could simplify your current project?

Implementing this took our application from handling hundreds to millions of tasks daily. The type safety caught numerous bugs during development, and Redis’s performance surprised even our skeptical DevOps team. Give it a try - start with a simple queue for your next batch job.

Found this useful? Share it with your team and leave a comment about your queue experiences! What challenges have you faced with background jobs? Let’s discuss below.

Keywords: BullMQ distributed task queue, Redis task queue TypeScript, distributed task queue system, BullMQ Redis TypeScript tutorial, Node.js task queue implementation, scalable job processing system, async task queue architecture, BullMQ worker scaling, Redis queue monitoring, TypeScript job processors



Similar Posts
Blog Image
Complete Guide to Next.js and Prisma Integration: Build Type-Safe Full-Stack Apps in 2024

Learn how to integrate Next.js with Prisma for powerful full-stack development. Build type-safe apps with unified TypeScript codebase and seamless database management.

Blog Image
Build High-Performance Event Sourcing Systems: Node.js, TypeScript, and EventStore Complete Guide

Learn to build a high-performance event sourcing system with Node.js, TypeScript, and EventStore. Master CQRS patterns, event versioning, and production deployment.

Blog Image
Build Multi-Tenant SaaS with NestJS, Prisma: Complete Database-per-Tenant Architecture Guide

Learn to build scalable multi-tenant SaaS apps with NestJS, Prisma & database-per-tenant architecture. Master dynamic connections, security & automation.

Blog Image
Complete Event-Driven Microservices Guide: NestJS, RabbitMQ, MongoDB with Distributed Transactions and Monitoring

Learn to build scalable event-driven microservices with NestJS, RabbitMQ & MongoDB. Master event sourcing, distributed transactions & monitoring for production systems.

Blog Image
Build Modern Full-Stack Apps: Complete Svelte and Supabase Integration Guide for Real-Time Development

Build modern full-stack apps with Svelte and Supabase integration. Learn real-time data sync, seamless auth, and reactive UI patterns for high-performance web applications.

Blog Image
Build Offline-First Desktop Apps with Electron and Sequelize

Learn how to create cross-platform desktop apps using web skills and local databases with Electron and Sequelize.