Email Queue Processing
BullMQ queue for emails, templates, retry logic, and bounce handling
What You’ll Build
After following this guide, you will have a working implementation of email queue processing in your project. Never lose an email by processing them through a reliable queue. Instead of sending emails directly in your request handlers (which can fail silently), add them to a BullMQ queue. Failed emails are automatically retried with exponential backoff.
Use Cases & Problems Solved
- Send transactional emails reliably without landing in spam folders
- Set up notification systems that scale with your user base
- Avoid building email delivery infrastructure from scratch
Prerequisites
- Redis server
- BullMQ
- Email provider (Resend, SendGrid, etc.)
Step-by-Step Implementation
Email queue setup
The following snippet shows how to email queue setup. Copy this into your project and adjust the values for your environment.
import { Queue, Worker } from 'bullmq';
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
const emailQueue = new Queue('emails', {
connection: { host: 'localhost', port: 6379 },
defaultJobOptions: {
attempts: 5,
backoff: { type: 'exponential', delay: 5000 },
removeOnComplete: { age: 86400 }, // Keep for 24h
removeOnFail: { age: 604800 }, // Keep failed for 7 days
},
});
// Worker processes emails
const worker = new Worker('emails', async (job) => {
const { to, subject, template, data } = job.data;
const html = renderTemplate(template, data);
const result = await resend.emails.send({ from: 'app@yourdomain.com', to, subject, html });
return { messageId: result.data.id };
}, { connection: { host: 'localhost', port: 6379 }, concurrency: 10 });
// Add email to queue (use this instead of sending directly)
export async function queueEmail(to, subject, template, data) {
await emailQueue.add('send', { to, subject, template, data });
}
⚠️ Don’t Do This
❌ Sending emails in the request handler without error handling
app.post('/signup', async (req, res) => {
await createUser(req.body);
await sendEmail(req.body.email, 'Welcome!'); // If this fails, email is LOST
res.json({ ok: true });
});
✅ Queue the email — it will be retried automatically if it fails
app.post('/signup', async (req, res) => {
await createUser(req.body);
await queueEmail(req.body.email, 'Welcome!', 'welcome', {}); // Never lost
res.json({ ok: true });
});
// Failed emails are retried 5 times with exponential backoff
Testing
Verify your implementation with these tests:
// __tests__/email-queue-processing.test.ts
import { describe, it, expect } from 'vitest';
describe('Email Queue Processing', () => {
it('should initialize without errors', () => {
// Test that the setup completes successfully
expect(() => setup()).not.toThrow();
});
it('should handle the primary use case', async () => {
const result = await execute();
expect(result).toBeDefined();
expect(result.success).toBe(true);
});
it('should handle edge cases', async () => {
// Test with empty/null input
const result = await execute(null);
expect(result.error).toBeDefined();
});
});
Verification
# Start the worker:
node email-worker.js
# Queue a test email:
node -e "require('./emailQueue').queueEmail('test@example.com', 'Test', 'welcome', {})"
# Check BullMQ dashboard:
npx bull-board
# Should show completed/failed jobs with details Related Specs
Beginner
Email Templates with React Email
Responsive templates, components, preview server, and testing
Email & Notifications
Beginner
Send Emails with Resend
React Email templates, attachments, batch sending, and delivery tracking
Email & Notifications
Email & Notifications