Building a Robust Email Queue System with BullMQ and Redis
Learn how to implement a production-ready email queue system using BullMQ and Redis in your Next.js application. Handle retries, delays, and failures gracefully.
December 6, 2024
4 min read
By FastSaaS Team
Building a Robust Email Queue System with BullMQ and Redis
Sending emails directly from your API can cause timeouts and poor user experience. A queue system solves this by processing emails asynchronously in the background.
Why Use Email Queues?
- Improved response times - API returns immediately
- Automatic retries - Failed emails retry automatically
- Rate limiting - Control email sending speed
- Reliability - Emails persist even if server restarts
Architecture Overview
User Action → API Endpoint → Add to Queue → Queue Worker → Email Provider
↓
Redis Store
Setup
Install Dependencies
npm install bullmq ioredis resend
Configure Redis Connection
// lib/redis.ts
import { Redis } from "ioredis";
export const redis = new Redis(
process.env.REDIS_URL || "redis://localhost:6379"
);
Create Email Queue
// lib/queue/email-queue.ts
import { Queue, Worker, Job } from "bullmq";
import { redis } from "../redis";
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
export interface EmailJob {
to: string;
subject: string;
html: string;
from?: string;
replyTo?: string;
}
// Create the queue
export const emailQueue = new Queue<EmailJob>("emails", {
connection: redis,
defaultJobOptions: {
attempts: 3,
backoff: {
type: "exponential",
delay: 1000,
},
removeOnComplete: 100, // Keep last 100 completed
removeOnFail: 1000, // Keep last 1000 failed
},
});
// Create the worker
export const emailWorker = new Worker<EmailJob>(
"emails",
async (job: Job<EmailJob>) => {
const { to, subject, html, from, replyTo } = job.data;
console.log(`Processing email job ${job.id} to ${to}`);
const { data, error } = await resend.emails.send({
from: from || "FastSaaS <noreply@fastsaas.cloud>",
to,
subject,
html,
replyTo,
});
if (error) {
console.error(`Email failed: ${error.message}`);
throw error; // Triggers retry
}
console.log(`Email sent successfully: ${data?.id}`);
return data;
},
{
connection: redis,
concurrency: 5, // Process 5 emails at a time
}
);
Adding Emails to Queue
// lib/email/send-email.ts
import { emailQueue } from "../queue/email-queue";
export async function sendEmail(options: {
to: string;
subject: string;
html: string;
delay?: number;
priority?: number;
}) {
const { to, subject, html, delay, priority } = options;
const job = await emailQueue.add(
"send-email",
{ to, subject, html },
{
delay, // Delay in milliseconds
priority, // Lower = higher priority
}
);
return job.id;
}
// Usage examples
// Immediate send
await sendEmail({
to: "user@example.com",
subject: "Welcome!",
html: "<h1>Welcome to FastSaaS!</h1>",
});
// Delayed send (1 hour)
await sendEmail({
to: "user@example.com",
subject: "How are you doing?",
html: "<p>Just checking in...</p>",
delay: 60 * 60 * 1000,
});
// High priority (payment confirmations)
await sendEmail({
to: "user@example.com",
subject: "Payment Received",
html: "<p>Thank you for your purchase!</p>",
priority: 1,
});
Email Templates
// lib/email/templates.ts
export const templates = {
welcome: (name: string) => ({
subject: `Welcome to FastSaaS, ${name}!`,
html: `
<h1>Welcome, ${name}!</h1>
<p>We're excited to have you on board.</p>
<a href="https://fastsaas.cloud/dashboard">Go to Dashboard</a>
`,
}),
passwordReset: (resetUrl: string) => ({
subject: "Reset Your Password",
html: `
<h1>Password Reset</h1>
<p>Click the link below to reset your password:</p>
<a href="${resetUrl}">Reset Password</a>
<p>This link expires in 1 hour.</p>
`,
}),
purchaseConfirmation: (product: string, amount: number) => ({
subject: "Purchase Confirmed!",
html: `
<h1>Thank You for Your Purchase!</h1>
<p>You've purchased: ${product}</p>
<p>Amount: $${amount}</p>
`,
}),
};
Queue Dashboard API
// app/api/admin/email-queue/route.ts
import { NextResponse } from "next/server";
import { emailQueue } from "@/lib/queue/email-queue";
export async function GET() {
const [waiting, active, completed, failed] = await Promise.all([
emailQueue.getWaitingCount(),
emailQueue.getActiveCount(),
emailQueue.getCompletedCount(),
emailQueue.getFailedCount(),
]);
const recentJobs = await emailQueue.getJobs(
["completed", "failed", "waiting", "active"],
0,
10
);
return NextResponse.json({
counts: { waiting, active, completed, failed },
recentJobs: recentJobs.map((job) => ({
id: job.id,
name: job.name,
status: job.finishedOn
? "completed"
: job.processedOn
? "active"
: "waiting",
to: job.data.to,
subject: job.data.subject,
attempts: job.attemptsMade,
createdAt: job.timestamp,
})),
});
}
Event Handling
// Handle queue events
emailWorker.on("completed", (job) => {
console.log(`✅ Email ${job.id} sent to ${job.data.to}`);
});
emailWorker.on("failed", (job, error) => {
console.error(`❌ Email ${job?.id} failed: ${error.message}`);
// Could notify admin or log to error tracking
});
emailWorker.on("stalled", (jobId) => {
console.warn(`⚠️ Email ${jobId} stalled`);
});
Production Considerations
- Run worker in separate process - Don't block your web server
- Monitor queue health - Set up alerts for failures
- Handle duplicates - Use unique job IDs
- Clean up old jobs - Prevent Redis memory bloat
Conclusion
A queue-based email system is essential for production SaaS applications. FastSaaS includes this complete implementation out of the box.
Get FastSaaS with built-in email queue system!