javascript 95 lines · 3 tabs

Reliable Background Email Jobs With BullMQ, Redis, and a Worker Process

Shared by codesnips Jul 2026
3 tabs
const express = require('express');
const { enqueueEmail } = require('./emailQueue');

const router = express.Router();

router.post('/users/:id/welcome-email', async (req, res, next) => {
  try {
    const userId = req.params.id;
    const { email } = req.body;
    if (!email) {
      return res.status(422).json({ error: 'email is required' });
    }

    const job = await enqueueEmail({
      type: 'welcome',
      to: email,
      userId,
      dedupeKey: `welcome:${userId}`,
    });

    return res.status(202).json({ queued: true, jobId: job.id });
  } catch (err) {
    return next(err);
  }
});

module.exports = router;
3 files · javascript Explain with highlit

This snippet shows how a durable background-email pipeline is wired together with BullMQ on top of Redis, split across the three pieces that actually collaborate in production: a shared queue definition, the enqueue path used by request handlers, and the standalone worker process that drains the queue.

In emailQueue.js the queue is created once and exported so every part of the app shares a single BullMQ Queue instance backed by one Redis connection. The defaultJobOptions encode the reliability policy in one place: attempts with backoff set to exponential means a transient SMTP failure is retried on a widening schedule rather than dropped, while removeOnComplete keeps Redis from growing unbounded and removeOnFail retains recent failures for inspection. enqueueEmail wraps queue.add and passes a stable jobId derived from the caller's dedupeKey, which is the core idempotency trick — BullMQ refuses to insert a second job with the same id, so a retried HTTP request or an at-least-once upstream event cannot enqueue the same welcome email twice.

In emailController.js the Express handler does the minimum synchronous work: it validates input, then calls enqueueEmail and returns 202 Accepted immediately. The request never waits on the mail server, so latency stays low and a slow provider cannot tie up web workers. The dedupeKey is built from the user id and email type so repeated clicks collapse to one job.

emailWorker.js is a separate long-lived process, not part of the web server. The Worker binds to the same queue name and Redis, and its concurrency option controls how many messages are processed in parallel per worker — horizontal scale comes from running more worker processes. The processor switches on job.data.type, sends through a mailer, and lets thrown errors propagate so BullMQ can apply the retry policy; a handled error would wrongly mark the job complete. The failed and completed listeners provide observability, and job.attemptsMade distinguishes a first failure from an exhausted one.

The main trade-off is operational: a second process and a Redis dependency in exchange for decoupled, retryable, deduplicated delivery. This pattern is the right reach whenever a request triggers slow, failure-prone I/O that should not block the response.


Related snips

Share this code

Here's the card — post it anywhere.

Reliable Background Email Jobs With BullMQ, Redis, and a Worker Process — share card
Link copied