TrueSpec

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
Beginner

SMS Notifications with Twilio

Send/receive SMS, verification codes, and webhook handling

Email & Notifications