agenda, bee-queue, bull, bullmq, kue, and node-resque are all Node.js libraries for managing background job queues — essential for offloading time-consuming tasks like sending emails, processing files, or syncing data. They enable reliable, asynchronous task execution with features like retries, delays, and failure handling. While they share the same goal, they differ significantly in architecture (MongoDB vs Redis), API design, maintenance status, and advanced capabilities like job prioritization or TypeScript support.
When you need to run background tasks in a Node.js application — like sending emails, processing images, or syncing data — you’ll likely reach for a job queue. The six packages compared here (agenda, bee-queue, bull, bullmq, kue, and node-resque) all solve this problem but with different trade-offs in architecture, features, and maintenance status. Let’s dive into how they differ and which one fits your needs.
Before comparing features, it’s critical to know that kue is officially deprecated. Its GitHub repository states: “Kue is no longer maintained.” You should not use kue in new projects. Similarly, while agenda and node-resque are still installable, their development has slowed significantly, and they lack modern features like TypeScript support or active issue triage.
That leaves bee-queue, bull, and bullmq as the actively maintained options worth serious consideration today.
The biggest architectural split among these libraries is the underlying data store they require.
agenda and node-resque use MongoDB:
// agenda: Requires MongoDB connection
const agenda = new Agenda({ db: { address: 'mongodb://127.0.0.1/agendaDb' } });
agenda.define('send email', async (job) => {
await sendEmail(job.attrs.data.to);
});
// node-resque: Also MongoDB-based
const worker = new NR.Worker({
connection: { host: '127.0.0.1', port: 27017 },
queues: ['email'],
timeout: 5000
});
bee-queue, bull, and bullmq use Redis:
// bee-queue: Redis-backed
const Queue = require('bee-queue');
const emailQueue = new Queue('emails', { redis: { host: '127.0.0.1' } });
emailQueue.process(async (job) => {
return await sendEmail(job.data.to);
});
// bull: Redis-backed
const Queue = require('bull');
const emailQueue = new Queue('emails'); // Uses default Redis connection
emailQueue.process(async (job) => {
await sendEmail(job.data.to);
});
// bullmq: Redis-backed, TypeScript-native
import { Worker } from 'bullmq';
const worker = new Worker('emails', async (job) => {
await sendEmail(job.data.to);
});
💡 Recommendation: Unless you have strong constraints against Redis, choose a Redis-based queue. It’s the industry standard for job queues.
How you define and handle jobs varies significantly.
agenda and kue use callback-style APIs (though agenda supports async):
// kue (deprecated)
const q = require('kue').createQueue();
q.process('email', (job, done) => {
sendEmail(job.data.to, (err) => {
if (err) return done(err);
done();
});
});
bee-queue, bull, and bullmq fully embrace Promises and async/await:
// bull
queue.process('email', async (job) => {
await sendEmail(job.data.to); // throws → job fails
});
// bullmq
new Worker('email', async (job) => {
await sendEmail(job.data.to);
});
All modern queues support retries and delays, but implementation differs.
Retry strategies:
bull and bullmq let you configure backoff strategies (exponential, fixed).bee-queue supports simple retry counts.agenda supports retries but with less flexibility.// bull: Exponential backoff
queue.add('work', { foo: 'bar' }, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000
}
});
// bullmq: Same concept, cleaner syntax
await queue.add('work', { foo: 'bar' }, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000
}
});
// bee-queue: Basic retries
queue.createJob({ foo: 'bar' })
.retries(3)
.save();
Job priorities:
bull and bullmq support job priorities out of the box.// bullmq: Priority support
await queue.add('work', { foo: 'bar' }, { priority: 1 }); // 1 = highest
Visibility into your queue health matters in production.
bull and bullmq integrate with Bull Board, a dashboard showing active, completed, and failed jobs.agenda has third-party UIs like agenda-ui, but they’re less polished.bee-queue has no official UI — you’d need to build your own monitoring.kue had a built-in UI, but it’s obsolete now.// bull board setup (for bull or bullmq)
import { createBullBoard } from '@bull-board/api';
import { BullAdapter } from '@bull-board/bull';
createBullBoard({
queues: [new BullAdapter(myQueue)]
});
bullmq is written in TypeScript — full type safety out of the box.bull has community-maintained types via @types/bull.bee-queue, agenda, kue, and node-resque lack official TypeScript definitions.If your project uses TypeScript, bullmq gives you the smoothest experience.
bullmq is the official successor to bull, built on the same core ideas but rewritten with TypeScript and improved internals. Key differences:
bull uses a single Queue class for both producing and consuming.bullmq separates concerns: Queue (producer), Worker (consumer), QueueScheduler (optional for delayed jobs).// bull: Single class
const queue = new Queue('work');
queue.process(async (job) => { /*...*/ });
queue.add('work', { data: 1 });
// bullmq: Separated roles
const queue = new Queue('work');
const worker = new Worker('work', async (job) => { /*...*/ });
await queue.add('work', { data: 1 });
This separation makes testing and scaling easier but adds slight verbosity.
kueagendanode-resquebullmq if you want the most modern, well-maintained, TypeScript-friendly option with full feature parity (retries, priorities, delays, UI).bull if you’re migrating an existing codebase or prefer its simpler API and don’t need TypeScript.bee-queue only if you need a minimal, lightweight queue without priorities or complex UI — but know you’ll miss out on ecosystem tooling.bull, plan a migration to bullmq for long-term maintainability.agenda or kue, migrate to bullmq — the Redis switch is worth it for reliability and performance.| Package | Data Store | Actively Maintained? | TypeScript | Priority Jobs | Backoff Strategies | UI Available |
|---|---|---|---|---|---|---|
agenda | MongoDB | ❌ (Stale) | ❌ | ❌ | Limited | Third-party |
bee-queue | Redis | ✅ | ❌ | ❌ | Basic | ❌ |
bull | Redis | ✅ | ✅ (via DT) | ✅ | ✅ | ✅ (Bull Board) |
bullmq | Redis | ✅ | ✅ (Native) | ✅ | ✅ | ✅ (Bull Board) |
kue | Redis | ❌ (Deprecated) | ❌ | ✅ | ❌ | ✅ (Obsolete) |
node-resque | Redis | ❌ (Stale) | ❌ | ❌ | ❌ | ❌ |
Job queues are infrastructure — once you pick one, switching is painful. Go with bullmq for new work. It’s the future-proof choice that balances power, simplicity, and active maintenance. Avoid deprecated packages unless you’re maintaining legacy code with no upgrade path.
Choose agenda only if you're already deeply invested in MongoDB and cannot introduce Redis, and you're maintaining a legacy system where migration isn't feasible. Avoid it for new projects due to stalled development and lack of modern features like native TypeScript support or priority queues.
Choose bee-queue if you need a lightweight, Redis-based queue with a simple API and don't require advanced features like job priorities or a monitoring UI. It's suitable for small-to-medium applications where minimal dependencies and straightforward retry logic are sufficient.
Choose bull if you want a mature, Redis-based queue with a rich feature set (retries with backoff, priorities, delays) and good ecosystem support like Bull Board for monitoring. It's ideal for teams not yet using TypeScript or those migrating from older queue systems who want a stable, proven solution.
Choose bullmq for all new projects requiring a job queue. It's the actively maintained, TypeScript-native successor to bull, offering the same powerful features (priorities, advanced backoff, UI integration) with better architecture, type safety, and long-term support. It's the best balance of modern tooling and production readiness.
Do not choose kue for any new project — it is officially deprecated and no longer maintained. If you encounter it in legacy code, plan a migration to bullmq. Its callback-based API, lack of TypeScript support, and obsolete UI make it unsuitable for modern development.
Avoid node-resque for new work. While it offers Redis-backed queuing inspired by Ruby's Resque, it suffers from minimal maintenance, poor documentation, and no TypeScript support. Only consider it if you're extending an existing system that already uses it and migration isn't possible.
A light-weight job scheduling library for Node.js
Migrating from v5? See the Migration Guide for all breaking changes.
Agenda 6.x is a complete TypeScript rewrite with a focus on modularity and flexibility:
Pluggable storage backends - Choose from MongoDB, PostgreSQL, Redis, or implement your own. Each backend is a separate package - install only what you need.
Pluggable notification channels - Move beyond polling with real-time job notifications via Redis, PostgreSQL LISTEN/NOTIFY, or other pub/sub systems. Jobs get processed immediately when saved, not on the next poll cycle.
Modern stack - ESM-only, Node.js 18+, full TypeScript with strict typing.
See the 6.x Roadmap for details and progress.
Install the core package and your preferred backend:
# For MongoDB
npm install agenda @agendajs/mongo-backend
# For PostgreSQL
npm install agenda @agendajs/postgres-backend
# For Redis
npm install agenda @agendajs/redis-backend
Requirements:
import { Agenda } from 'agenda';
import { MongoBackend } from '@agendajs/mongo-backend';
const agenda = new Agenda({
backend: new MongoBackend({ address: 'mongodb://localhost/agenda' })
});
// Define a job
agenda.define('send email', async (job) => {
const { to, subject } = job.attrs.data;
await sendEmail(to, subject);
});
// Start processing
await agenda.start();
// Schedule jobs
await agenda.every('1 hour', 'send email', { to: 'user@example.com', subject: 'Hello' });
await agenda.schedule('in 5 minutes', 'send email', { to: 'admin@example.com', subject: 'Report' });
await agenda.now('send email', { to: 'support@example.com', subject: 'Urgent' });
| Package | Backend | Notifications | Install |
|---|---|---|---|
@agendajs/mongo-backend | MongoDB | Polling only | npm install @agendajs/mongo-backend |
@agendajs/postgres-backend | PostgreSQL | LISTEN/NOTIFY | npm install @agendajs/postgres-backend |
@agendajs/redis-backend | Redis | Pub/Sub | npm install @agendajs/redis-backend |
| Backend | Storage | Notifications | Notes |
|---|---|---|---|
MongoDB (MongoBackend) | ✅ | ❌ | Storage only. Combine with external notification channel for real-time. |
PostgreSQL (PostgresBackend) | ✅ | ✅ | Full backend. Uses LISTEN/NOTIFY for notifications. |
Redis (RedisBackend) | ✅ | ✅ | Full backend. Uses Pub/Sub for notifications. |
| InMemoryNotificationChannel | ❌ | ✅ | Notifications only. For single-process/testing. |
import { Agenda } from 'agenda';
import { MongoBackend } from '@agendajs/mongo-backend';
// Via connection string
const agenda = new Agenda({
backend: new MongoBackend({ address: 'mongodb://localhost/agenda' })
});
// Via existing MongoDB connection
const agenda = new Agenda({
backend: new MongoBackend({ mongo: existingDb })
});
// With options
const agenda = new Agenda({
backend: new MongoBackend({
mongo: db,
collection: 'jobs' // Collection name (default: 'agendaJobs')
}),
processEvery: '30 seconds', // Job polling interval
maxConcurrency: 20, // Max concurrent jobs
defaultConcurrency: 5 // Default per job type
});
import { Agenda } from 'agenda';
import { PostgresBackend } from '@agendajs/postgres-backend';
const agenda = new Agenda({
backend: new PostgresBackend({
connectionString: 'postgresql://user:pass@localhost:5432/mydb'
})
});
import { Agenda } from 'agenda';
import { RedisBackend } from '@agendajs/redis-backend';
const agenda = new Agenda({
backend: new RedisBackend({
connectionString: 'redis://localhost:6379'
})
});
For faster job processing across distributed systems:
import { Agenda, InMemoryNotificationChannel } from 'agenda';
import { MongoBackend } from '@agendajs/mongo-backend';
const agenda = new Agenda({
backend: new MongoBackend({ mongo: db }),
notificationChannel: new InMemoryNotificationChannel()
});
You can use MongoDB for storage while using a different system for real-time notifications:
import { Agenda } from 'agenda';
import { MongoBackend } from '@agendajs/mongo-backend';
import { RedisBackend } from '@agendajs/redis-backend';
// MongoDB for storage + Redis for real-time notifications
const redisBackend = new RedisBackend({ connectionString: 'redis://localhost:6379' });
const agenda = new Agenda({
backend: new MongoBackend({ mongo: db }),
notificationChannel: redisBackend.notificationChannel
});
This is useful when you want MongoDB's proven durability and flexible queries for job storage, but need faster real-time notifications across multiple processes.
// Simple async handler
agenda.define('my-job', async (job) => {
console.log('Processing:', job.attrs.data);
});
// With options
agenda.define('my-job', async (job) => { /* ... */ }, {
concurrency: 10,
lockLimit: 5,
lockLifetime: 10 * 60 * 1000, // 10 minutes
priority: 'high'
});
For a class-based approach, use TypeScript decorators:
import { JobsController, Define, Every, registerJobs, Job } from 'agenda';
@JobsController({ namespace: 'email' })
class EmailJobs {
@Define({ concurrency: 5 })
async sendWelcome(job: Job<{ userId: string }>) {
console.log('Sending welcome to:', job.attrs.data.userId);
}
@Every('1 hour')
async cleanupBounced(job: Job) {
console.log('Cleaning up bounced emails');
}
}
registerJobs(agenda, [new EmailJobs()]);
await agenda.start();
// Schedule using namespaced name
await agenda.now('email.sendWelcome', { userId: '123' });
See Decorators Documentation for full details.
// Run immediately
await agenda.now('my-job', { userId: '123' });
// Run at specific time
await agenda.schedule('tomorrow at noon', 'my-job', data);
await agenda.schedule(new Date('2024-12-25'), 'my-job', data);
// Run repeatedly
await agenda.every('5 minutes', 'my-job');
await agenda.every('0 * * * *', 'my-job'); // Cron syntax
// Cancel jobs (removes from database)
await agenda.cancel({ name: 'my-job' });
// Disable/enable jobs globally (by query)
await agenda.disable({ name: 'my-job' }); // Disable all jobs matching query
await agenda.enable({ name: 'my-job' }); // Enable all jobs matching query
// Disable/enable individual jobs
const job = await agenda.create('my-job', data);
job.disable();
await job.save();
// Progress tracking
agenda.define('long-job', async (job) => {
for (let i = 0; i <= 100; i += 10) {
await doWork();
await job.touch(i); // Report progress 0-100
}
});
// Stop immediately - unlocks running jobs so other workers can pick them up
await agenda.stop();
// Drain - waits for running jobs to complete before stopping
await agenda.drain();
// Drain with timeout (30 seconds) - for cloud platforms with shutdown deadlines
const result = await agenda.drain(30000);
if (result.timedOut) {
console.log(`${result.running} jobs still running after timeout`);
}
// Drain with AbortSignal - for external control
const controller = new AbortController();
setTimeout(() => controller.abort(), 30000);
await agenda.drain({ signal: controller.signal });
Use drain() for graceful shutdowns where you want in-progress jobs to finish their work.
agenda.on('start', (job) => console.log('Job started:', job.attrs.name));
agenda.on('complete', (job) => console.log('Job completed:', job.attrs.name));
agenda.on('success', (job) => console.log('Job succeeded:', job.attrs.name));
agenda.on('fail', (err, job) => console.log('Job failed:', job.attrs.name, err));
// Job-specific events
agenda.on('start:send email', (job) => { /* ... */ });
agenda.on('fail:send email', (err, job) => { /* ... */ });
For databases other than MongoDB, PostgreSQL, or Redis, implement AgendaBackend:
import { AgendaBackend, JobRepository } from 'agenda';
class SQLiteBackend implements AgendaBackend {
readonly repository: JobRepository;
readonly notificationChannel = undefined; // Or implement NotificationChannel
async connect() { /* ... */ }
async disconnect() { /* ... */ }
}
const agenda = new Agenda({
backend: new SQLiteBackend({ path: './jobs.db' })
});
See Custom Backend Driver for details.
Official Backend Packages:
Tools:
MIT