Skip to main content
← All Articles Β· Β· 12 min read

Temporal.io vs BullMQ vs Agenda: Node.js Job Queue 2026

Compare Temporal.io, BullMQ, and Agenda for Node.js job queues in 2026. Features, scalability, code examples, and which to choose for your project.

nodejsjob-queuebullmqtemporalagendadistributed-systemsbackendredis

Background jobs are the unsung backbone of modern web applications. Every time a user signs up and receives a welcome email, an image gets resized, a report gets generated overnight, or a payment is retried after a failure β€” a job queue is doing that work.

In Node.js, three tools dominate this space in 2026: BullMQ, Temporal.io, and Agenda. They solve the same core problem in fundamentally different ways, at different scales, with different operational complexity.

This guide breaks down all three so you can pick the right one for your use case.


Why Job Queues Matter in Distributed Systems

A synchronous HTTP request has a time limit (usually 30–60 seconds). Users expect a response in milliseconds. These constraints collide the moment you need to:

  • Send transactional emails
  • Process uploaded files
  • Call external APIs (which may fail and need retry)
  • Schedule recurring tasks (cron-style)
  • Run multi-step workflows that span hours or days

A job queue decouples the task from the HTTP request. The web server enqueues a job and responds immediately. A worker process picks it up, executes it, handles retries if it fails, and reports the result β€” all asynchronously.

The question isn’t whether you need a job queue. It’s which one fits your team, stack, and scale.


Quick Comparison Table

FeatureBullMQTemporal.ioAgenda
LanguageTypeScript/JS nativeMulti-language (Node, Go, Python, Java)TypeScript/JS native
PersistenceRedis (required)Temporal Server + DB (Postgres/MySQL/SQLite)MongoDB (required)
SchedulingDelayed jobs, cronCron, timers, sleepCron, date-based
RetriesBuilt-in, configurableBuilt-in with historyBuilt-in, configurable
Durable executionNoYes (survives crashes mid-workflow)No
Multi-step workflowsManual (parent/child jobs)First-class (workflows + activities)No
UI dashboardBull Board, ArenaTemporal WebNone built-in
ScalabilityHigh (horizontal workers)Very high (microservices-grade)Medium
Operational complexityLow–MediumHighLow
Bundle size~200 KBLarger (SDK + server)~100 KB
Best forMicroservices, high-volume queuesLong-running workflows, distributed sagasSimple apps, Mongo stacks

BullMQ: Redis-Based, Production-Ready

BullMQ is the modern successor to Bull. It uses Redis as both a message broker and storage layer. If you’re building microservices and need a battle-tested, high-throughput queue, BullMQ is the first tool to reach for.

Core Concepts

  • Queue: named channel for jobs (e.g., email-queue, resize-queue)
  • Worker: process that consumes jobs from a queue
  • Job: a unit of work with payload, options, and lifecycle
  • Flow: parent/child job dependencies (added in BullMQ)

Installation

npm install bullmq
# Requires Redis running locally or via Docker
docker run -p 6379:6379 redis:alpine

Basic Producer + Consumer

// producer.ts
import { Queue } from 'bullmq';

const emailQueue = new Queue('emails', {
  connection: { host: 'localhost', port: 6379 },
});

await emailQueue.add('welcome-email', {
  to: '[email protected]',
  name: 'Alice',
});

console.log('Job enqueued');
// worker.ts
import { Worker } from 'bullmq';

const worker = new Worker(
  'emails',
  async (job) => {
    const { to, name } = job.data;
    console.log(`Sending welcome email to ${name} at ${to}`);
    // await sendEmail(to, name);
  },
  {
    connection: { host: 'localhost', port: 6379 },
    concurrency: 10,
  }
);

worker.on('completed', (job) => {
  console.log(`Job ${job.id} completed`);
});

worker.on('failed', (job, err) => {
  console.error(`Job ${job?.id} failed: ${err.message}`);
});

Retry with Backoff

await emailQueue.add(
  'welcome-email',
  { to: '[email protected]', name: 'Alice' },
  {
    attempts: 5,
    backoff: {
      type: 'exponential',
      delay: 2000, // 2s, 4s, 8s, 16s, 32s
    },
    removeOnComplete: 100, // keep last 100 completed jobs
    removeOnFail: 200,
  }
);

Repeatable (Cron) Jobs

await emailQueue.add(
  'daily-digest',
  { type: 'digest' },
  {
    repeat: {
      pattern: '0 8 * * *', // every day at 8 AM
    },
  }
);

When to Use BullMQ

  • High-volume task processing (thousands of jobs/second with Redis Cluster)
  • Microservices needing decoupled async tasks
  • Teams already running Redis
  • You want a mature ecosystem with monitoring UIs (Bull Board)

Limitations

  • Redis is a hard dependency (cost + ops)
  • No built-in support for multi-step workflows spanning long time periods
  • No durable execution β€” if a worker crashes mid-job, the job may not resume from where it left off

Temporal.io: Durable Workflow Execution

Temporal.io takes a fundamentally different approach. Instead of a job queue, it’s a workflow orchestration platform. It gives you durable execution: your code can await something for days, survive server crashes, and resume exactly where it left off.

This is the tool you reach for when you need sagas, multi-step pipelines, or workflows that span hours or days.

Core Concepts

  • Workflow: a function that defines the sequence of steps; runs durably
  • Activity: an individual step (an external call, DB operation, etc.)
  • Worker: registers and executes workflows and activities
  • Temporal Server: the orchestration engine (self-hosted or Temporal Cloud)

Installation

npm install @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity

# Start Temporal dev server (includes UI at localhost:8233)
npx @temporalio/cli server start-dev

Activity + Workflow + Worker

// activities.ts
import { defineSignal } from '@temporalio/workflow';

export async function sendWelcomeEmail(to: string, name: string): Promise<void> {
  console.log(`Sending welcome email to ${name} at ${to}`);
  // await emailService.send(to, name);
}

export async function createUserRecord(userId: string): Promise<void> {
  console.log(`Creating user record for ${userId}`);
  // await db.users.create({ id: userId });
}
// workflows.ts
import { proxyActivities, sleep } from '@temporalio/workflow';
import type * as activities from './activities';

const { sendWelcomeEmail, createUserRecord } = proxyActivities<typeof activities>({
  startToCloseTimeout: '30 seconds',
  retry: {
    maximumAttempts: 5,
    initialInterval: '2 seconds',
    backoffCoefficient: 2,
  },
});

export async function onboardUser(userId: string, email: string, name: string): Promise<void> {
  // Step 1: Create record
  await createUserRecord(userId);

  // Step 2: Send welcome email
  await sendWelcomeEmail(email, name);

  // Step 3: Wait 3 days, send follow-up
  await sleep('3 days'); // This is durable β€” survives restarts

  await sendWelcomeEmail(email, `${name} (follow-up)`);
}
// worker.ts
import { Worker } from '@temporalio/worker';
import * as activities from './activities';

async function main() {
  const worker = await Worker.create({
    workflowsPath: require.resolve('./workflows'),
    activities,
    taskQueue: 'onboarding',
  });
  await worker.run();
}
main();
// client.ts
import { Client, Connection } from '@temporalio/client';
import { onboardUser } from './workflows';

const client = new Client({ connection: await Connection.connect() });

await client.workflow.start(onboardUser, {
  taskQueue: 'onboarding',
  workflowId: `onboard-user-${userId}`,
  args: [userId, '[email protected]', 'Alice'],
});

What Makes Temporal Different

The key insight: Temporal records every state transition. If your worker crashes during await sleep('3 days') at day 2, when the worker comes back up, Temporal replays the workflow history and resumes from exactly that point. No data lost.

This is called event sourcing for workflows β€” and it changes how you think about reliability.

When to Use Temporal

  • Multi-step workflows that need to survive failures
  • Long-running processes (hours, days, or weeks)
  • Distributed saga patterns (book flight + hotel + car, rollback any failure)
  • Complex retry logic with compensation steps
  • Teams building microservices that need guaranteed execution

Limitations

  • Requires running Temporal Server (self-hosted or Temporal Cloud, paid)
  • Higher operational complexity (new concept to learn)
  • Overkill for simple fire-and-forget tasks
  • Steeper learning curve vs. queue-based tools

Agenda: MongoDB-Based, Simple to Start

Agenda is the pragmatic choice if you’re already using MongoDB and need basic job scheduling without adding Redis or a full workflow engine to your stack.

Core Concepts

  • Jobs are stored directly in MongoDB
  • Supports cron-style and date-based scheduling
  • Lightweight API, minimal setup

Installation

npm install agenda
# Requires MongoDB

Basic Setup

import Agenda from 'agenda';
import { MongoClient } from 'mongodb';

const mongoConnectionString = 'mongodb://localhost:27017/myapp';

const agenda = new Agenda({ db: { address: mongoConnectionString } });

// Define a job
agenda.define('send-email', async (job) => {
  const { to, subject, body } = job.attrs.data;
  console.log(`Sending email to ${to}: ${subject}`);
  // await emailService.send({ to, subject, body });
});

// Start Agenda
await agenda.start();

// Schedule immediately
await agenda.now('send-email', {
  to: '[email protected]',
  subject: 'Welcome!',
  body: 'Hello from Agenda',
});

// Schedule in the future
await agenda.schedule('in 30 minutes', 'send-email', {
  to: '[email protected]',
  subject: 'Reminder',
  body: 'Just checking in',
});

// Cron schedule
await agenda.every('0 8 * * *', 'send-email', {
  to: '[email protected]',
  subject: 'Daily Digest',
  body: 'Here is your daily digest...',
});

Retry on Failure

agenda.define(
  'send-email',
  { priority: 'high', concurrency: 5 },
  async (job) => {
    try {
      await sendEmail(job.attrs.data);
    } catch (err) {
      // Agenda retries automatically if you throw
      throw err;
    }
  }
);

When to Use Agenda

  • Already using MongoDB and want to avoid another dependency
  • Simple scheduled tasks (cron jobs, delayed emails)
  • Early-stage applications or MVPs
  • Small teams that want minimal infrastructure

Limitations

  • No built-in UI dashboard
  • Weaker at high concurrency vs. BullMQ/Redis
  • MongoDB polling adds latency (default: every 16ms)
  • Less active maintenance compared to BullMQ
  • Not designed for complex multi-step workflows

Head-to-Head: Code Examples for the Same Task

Let’s compare all three solving the same task: retry an email 3 times with exponential backoff.

BullMQ:

await emailQueue.add('send-email', { to }, {
  attempts: 3,
  backoff: { type: 'exponential', delay: 1000 },
});

Temporal:

const { sendEmail } = proxyActivities<typeof activities>({
  startToCloseTimeout: '10 seconds',
  retry: { maximumAttempts: 3, initialInterval: '1 second', backoffCoefficient: 2 },
});
await sendEmail(to);

Agenda:

agenda.define('send-email', { priority: 'high' }, async (job) => {
  await sendEmail(job.attrs.data.to);
  // Throw on failure β€” Agenda will retry (configure max retries via job options)
});

BullMQ and Temporal both handle this cleanly with declarative config. Agenda’s retry story is less elegant out of the box.


Which One Should You Choose?

If you need…Use
Simple async tasks, already using RedisBullMQ
High-throughput microservices pipelinesBullMQ
Multi-step workflows that must survive crashesTemporal.io
Distributed sagas with rollbackTemporal.io
Simple scheduling, already using MongoDBAgenda
MVP / small project, minimal infraAgenda

Decision Tree

  1. Do you need durable execution or multi-step workflows? β†’ Temporal.io
  2. Are you already using Redis, or expect high volume? β†’ BullMQ
  3. Are you on MongoDB and need simple scheduling? β†’ Agenda

Migration Paths

Agenda β†’ BullMQ

If you outgrow Agenda’s throughput:

  1. Add Redis to your stack
  2. Install BullMQ
  3. Move job definitions one by one β€” API is similar enough
  4. Drain Agenda queues before cutting over

BullMQ β†’ Temporal

If you find yourself building complex multi-step workflows with BullMQ parent/child jobs:

  1. Set up Temporal Server (start with Temporal Cloud for zero ops)
  2. Model each BullMQ worker as a Temporal Activity
  3. Replace parent/child job chains with Workflow functions
  4. Migrate one workflow at a time

Production Tips

BullMQ:

  • Use removeOnComplete and removeOnFail to avoid Redis bloat
  • Enable Redis persistence (appendonly yes) to survive restarts
  • Run multiple workers for horizontal scaling

Temporal:

  • Start with Temporal Cloud during development β€” skip infra setup
  • Keep Activities idempotent (they may re-run on retry)
  • Use workflowId deterministically (e.g., onboard-{userId}) to prevent duplicates

Agenda:

  • Set appropriate lockLifetime to avoid jobs getting stuck
  • Index your agenda_jobs collection on nextRunAt and lockedAt
  • Use maxConcurrency to prevent overwhelming your server

Summary

BullMQTemporal.ioAgenda
ComplexityMediumHighLow
ScaleHighVery highMedium
Infra neededRedisTemporal ServerMongoDB
Ideal teamMicroservices teamsPlatform/infra teamsSolo devs, MVPs

For most Node.js teams in 2026, BullMQ is the default choice β€” mature, well-documented, and Redis is already common infrastructure. If your workflows are getting complex and durable execution matters, Temporal.io is worth the operational investment. For simple Mongo-based projects, Agenda remains a solid, zero-friction option.


Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.