bull vs agenda vs bee-queue vs kue vs node-resque vs bree
Node.js Job Queues and Task Scheduling
bullagendabee-queuekuenode-resquebree类似的npm包:

Node.js Job Queues and Task Scheduling

These libraries enable Node.js applications to handle background tasks, scheduled jobs, and heavy processing without blocking the main event loop. They provide structures for defining work, storing job state, and retrying failed tasks. While some rely on Redis for fast in-memory storage, others use MongoDB for durability or worker threads for local execution. Choosing the right tool depends on your infrastructure, reliability needs, and whether you require distributed processing or simple local scheduling.

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
bull2,245,94716,262309 kB1472 年前MIT
agenda228,6899,712310 kB1910 小时前MIT
bee-queue67,7854,039107 kB461 年前MIT
kue31,0799,418-2869 年前MIT
node-resque26,4421,415720 kB243 个月前Apache-2.0
bree03,29692 kB308 个月前MIT

Node.js Job Queues: Architecture and Implementation Compared

Handling background tasks is a common requirement in modern web applications. Whether you are sending emails, processing images, or running scheduled maintenance, you need a reliable way to manage work without blocking your main server. The packages agenda, bee-queue, bree, bull, kue, and node-resque all solve this problem, but they use different storage engines and architectural patterns. Let's compare how they handle job definition, execution, and storage.

🗄️ Storage Engine: Redis vs MongoDB vs Worker Threads

The underlying storage determines how jobs survive server restarts and how multiple servers share work.

bull and bee-queue rely on Redis.

  • Redis is fast and in-memory, making these queues great for high throughput.
  • Jobs are lost if Redis persists incorrectly, though both support persistence settings.
// bull: Redis connection
const Queue = require('bull');
const queue = new Queue('email', 'redis://127.0.0.1:6379');

// bee-queue: Redis connection
const Queue = require('bee-queue');
const queue = new Queue('email', { redis: { host: '127.0.0.1', port: 6379 } });

agenda uses MongoDB.

  • Jobs are stored as documents, so they survive restarts easily.
  • You can query jobs using standard Mongo queries.
// agenda: MongoDB connection
const Agenda = require('agenda');
const agenda = new Agenda({ db: { address: 'mongodb://127.0.0.1:27017/agenda' } });

bree uses Worker Threads locally.

  • It does not require an external database by default.
  • Jobs are defined as files and run in separate threads within the same process.
// bree: Local configuration
const Bree = require('bree');
const bree = new Bree({
  jobs: [
    { name: 'daily-cleanup', interval: '0 0 * * *', path: './jobs/cleanup.js' }
  ]
});

kue and node-resque also use Redis.

  • They follow older patterns of Redis list management.
  • kue is no longer maintained, so new projects should avoid it.
// kue: Redis connection (Legacy)
const kue = require('kue');
const queue = kue.createQueue();

// node-resque: Redis connection
const { Queue, Worker } = require('node-resque');
const queue = new Queue({ connection: { host: '127.0.0.1', port: 6379 } });

📥 Defining and Processing Jobs

Each library has a different way to define what work needs to be done and how to run it.

bull separates job addition from processing.

  • You define a processor function that runs when a job is available.
  • It supports concurrency limits directly in the processor.
// bull: Process jobs
queue.process('send-email', async (job) => {
  await sendEmail(job.data.to);
});

// bull: Add job
queue.add('send-email', { to: '[email protected]' });

agenda defines jobs by name and schedules them.

  • You define the logic once and can trigger it immediately or later.
  • It feels like a database collection of tasks.
// agenda: Define job
agenda.define('send-email', async (job) => {
  await sendEmail(job.attrs.data.to);
});

// agenda: Schedule job
agenda.now('send-email', { to: '[email protected]' });

bee-queue is similar to Bull but with a simpler API.

  • It focuses on minimal configuration and speed.
  • The processing model is straightforward.
// bee-queue: Process jobs
queue.process(async (job) => {
  await sendEmail(job.data.to);
});

// bee-queue: Create job
const job = queue.createJob({ to: '[email protected]' });
await job.save();

bree uses file-based jobs.

  • You write a script for each job type.
  • Bree manages the timing and execution of these scripts.
// bree: Job file (jobs/cleanup.js)
module.exports = async (config) => {
  await cleanupDatabase();
};

// bree: Start manager
await bree.start();

kue uses a creation chain pattern.

  • You build the job object step-by-step before saving.
  • This is verbose compared to modern alternatives.
// kue: Create job (Legacy)
const job = queue.create('send-email', { to: '[email protected]' });
job.save();

// kue: Process
queue.process('send-email', (job, done) => {
  sendEmail(job.data.to, done);
});

node-resque follows the classic Resque model.

  • You perform jobs based on class names.
  • It requires a separate worker process to listen for jobs.
// node-resque: Perform method
class SendEmail {
  async perform(to) {
    await sendEmail(to);
  }
}

// node-resque: Enqueue
await queue.enqueue('high', 'SendEmail', ['[email protected]']);

🕒 Scheduling and Recurrence

Some libraries handle cron jobs and delayed tasks better than others.

agenda excels at scheduling.

  • It has built-in support for every, schedule, and cron patterns.
  • You can query scheduled jobs easily via MongoDB.
// agenda: Recurring job
agenda.every('10 minutes', 'clear-cache');
agenda.schedule('in 2 hours', 'send-reminder', { userId: 123 });

bull supports delays and repeats.

  • You can set a job to run after a delay or on a cron schedule.
  • It relies on Redis timers for this functionality.
// bull: Delayed job
queue.add('notify', { id: 1 }, { delay: 5000 });

// bull: Repeated job
queue.add('report', {}, { repeat: { cron: '0 0 * * *' } });

bree is built for cron-style jobs.

  • It uses human-readable intervals or cron strings in config.
  • It is designed specifically for time-based execution.
// bree: Cron config
const bree = new Bree({
  jobs: [
    { name: 'backup', cron: '0 0 * * *', path: './jobs/backup.js' }
  ]
});

bee-queue, kue, and node-resque have limited scheduling.

  • They focus on immediate queue processing.
  • You often need an external scheduler (like node-cron) to add jobs at specific times.
// bee-queue: Delay only
const job = queue.createJob({}).delay(5000);
await job.save();

// kue: Delay only
queue.create('task', {}).delay(5000).save();

⚠️ Maintenance and Status

Choosing a library also means choosing its long-term support.

bull and agenda are actively maintained.

  • They receive regular updates and security patches.
  • They have large communities and many tutorials available.

bree is maintained and focused on simplicity.

  • It is a good choice for newer projects that want less infrastructure.
  • It avoids external dependencies like Redis.

bee-queue has slower maintenance cycles.

  • It is stable but updates are less frequent.
  • Suitable for stable projects that do not need cutting-edge features.

kue is deprecated.

  • It should not be used in new development.
  • Security vulnerabilities may not be fixed.

node-resque is stable but legacy.

  • It is best for maintaining older systems.
  • New projects might find bull more aligned with modern Node.js practices.

📊 Summary: Key Differences

Featurebullagendabreebee-queuekuenode-resque
StorageRedisMongoDBLocal/ThreadsRedisRedisRedis
SchedulingBuilt-inBuilt-inBuilt-inLimitedLimitedLimited
StatusActiveActiveActiveStableDeprecatedStable
ComplexityMediumMediumLowLowHighMedium
Best ForDistributedPersistenceCron/LocalSimplicityLegacyRuby Migration

💡 The Big Picture

bull is the safe bet for most distributed Node.js applications. It balances features, performance, and community support. If you need heavy scheduling and already use MongoDB, agenda is a strong contender. For simple cron jobs on a single server, bree removes the need for Redis entirely.

Avoid kue in new projects due to its deprecated status. Use node-resque only if you need compatibility with existing Resque infrastructure. For lightweight Redis needs where Bull feels too heavy, bee-queue remains an option, but verify its maintenance fits your risk tolerance.

Final Thought: The best queue is the one that matches your infrastructure. If you have Redis, use Bull. If you have Mongo, use Agenda. If you have neither, use Bree.

如何选择: bull vs agenda vs bee-queue vs kue vs node-resque vs bree

  • bull:

    Choose bull if you need a mature, feature-rich queue system backed by Redis. It supports priorities, retries, delays, and event listeners out of the box. It is the standard choice for distributed systems requiring high reliability and concurrency, though you should also evaluate BullMQ for modern Redis Stream support.

  • agenda:

    Choose agenda if your stack already uses MongoDB and you need robust scheduling features like cron jobs or recurring tasks. It stores job data in Mongo collections, making it easy to query and inspect jobs using standard database tools. It is well-suited for applications that prioritize data persistence over raw throughput.

  • bee-queue:

    Choose bee-queue if you need a lightweight, Redis-based queue with a focus on simplicity and performance. It has a smaller footprint than Bull and avoids complex Lua scripts. It is a good fit for projects that want Redis storage without the heavier feature set of Bull, though maintenance activity is slower.

  • kue:

    Do not choose kue for new projects. It is deprecated and no longer maintained. While it was popular historically for its Redis backend and UI, it lacks modern security updates and bug fixes. Existing projects should plan migration to bull or agenda.

  • node-resque:

    Choose node-resque if you are migrating from a Ruby on Rails Resque background job system and want to maintain compatibility. It follows the classic Resque pattern strictly. It is stable but less feature-rich than Bull for modern Node.js specific workflows.

  • bree:

    Choose bree if you need to run cron jobs or scheduled tasks using worker threads within a single Node.js process. It is designed for simplicity and does not require Redis or MongoDB. It is ideal for single-server deployments where you want to isolate jobs in separate threads to avoid blocking the main loop.

bull的README




The fastest, most reliable, Redis-based queue for Node.
Carefully written for rock solid stability and atomicity.


Sponsors · Features · UIs · Install · Quick Guide · Documentation

Check the new Guide!


🚀 Sponsors 🚀

Dragonfly Dragonfly is a new Redis™ drop-in replacement that is fully compatible with BullMQ and brings some important advantages over Redis™ such as massive better performance by utilizing all CPU cores available and faster and more memory efficient data structures. Read more here on how to use it with BullMQ.

📻 News and updates

Bull is currently in maintenance mode, we are only fixing bugs. For new features check BullMQ, a modern rewritten implementation in Typescript. You are still very welcome to use Bull if it suits your needs, which is a safe, battle tested library.

Follow me on Twitter for other important news and updates.

🛠 Tutorials

You can find tutorials and news in this blog: https://blog.taskforce.sh/


Used by

Bull is popular among large and small organizations, like the following ones:

Atlassian Autodesk Mozilla Nest Salesforce


Official FrontEnd

Taskforce.sh, Inc

Supercharge your queues with a professional front end:

  • Get a complete overview of all your queues.
  • Inspect jobs, search, retry, or promote delayed jobs.
  • Metrics and statistics.
  • and many more features.

Sign up at Taskforce.sh


Bull Features

  • Minimal CPU usage due to a polling-free design.
  • Robust design based on Redis.
  • Delayed jobs.
  • Schedule and repeat jobs according to a cron specification.
  • Rate limiter for jobs.
  • Retries.
  • Priority.
  • Concurrency.
  • Pause/resume—globally or locally.
  • Multiple job types per queue.
  • Threaded (sandboxed) processing functions.
  • Automatic recovery from process crashes.

And coming up on the roadmap...

  • Job completion acknowledgement (you can use the message queue pattern in the meantime).
  • Parent-child jobs relationships.

UIs

There are a few third-party UIs that you can use for monitoring:

BullMQ

Bull v3

Bull <= v2


Monitoring & Alerting


Feature Comparison

Since there are a few job queue solutions, here is a table comparing them:

FeatureBullMQ-ProBullMQBullKueBeeAgenda
Backendredisredisredisredisredismongo
Observables✓
Group Rate Limit✓
Group Support✓
Batches Support✓
Parent/Child Dependencies✓✓
Priorities✓✓✓✓✓
Concurrency✓✓✓✓✓✓
Delayed jobs✓✓✓✓✓
Global events✓✓✓✓
Rate Limiter✓✓✓
Pause/Resume✓✓✓✓
Sandboxed worker✓✓✓
Repeatable jobs✓✓✓✓
Atomic ops✓✓✓✓
Persistence✓✓✓✓✓✓
UI✓✓✓✓✓
Optimized forJobs / MessagesJobs / MessagesJobs / MessagesJobsMessagesJobs

Install

npm install bull --save

or

yarn add bull

Requirements: Bull requires a Redis version greater than or equal to 2.8.18.

Typescript Definitions

npm install @types/bull --save-dev
yarn add --dev @types/bull

Definitions are currently maintained in the DefinitelyTyped repo.

Contributing

We welcome all types of contributions, either code fixes, new features or doc improvements. Code formatting is enforced by prettier. For commits please follow conventional commits convention. All code must pass lint rules and test suites before it can be merged into develop.


Quick Guide

Basic Usage

const Queue = require('bull');

const videoQueue = new Queue('video transcoding', 'redis://127.0.0.1:6379');
const audioQueue = new Queue('audio transcoding', { redis: { port: 6379, host: '127.0.0.1', password: 'foobared' } }); // Specify Redis connection using object
const imageQueue = new Queue('image transcoding');
const pdfQueue = new Queue('pdf transcoding');

videoQueue.process(function (job, done) {

  // job.data contains the custom data passed when the job was created
  // job.id contains id of this job.

  // transcode video asynchronously and report progress
  job.progress(42);

  // call done when finished
  done();

  // or give an error if error
  done(new Error('error transcoding'));

  // or pass it a result
  done(null, { framerate: 29.5 /* etc... */ });

  // If the job throws an unhandled exception it is also handled correctly
  throw new Error('some unexpected error');
});

audioQueue.process(function (job, done) {
  // transcode audio asynchronously and report progress
  job.progress(42);

  // call done when finished
  done();

  // or give an error if error
  done(new Error('error transcoding'));

  // or pass it a result
  done(null, { samplerate: 48000 /* etc... */ });

  // If the job throws an unhandled exception it is also handled correctly
  throw new Error('some unexpected error');
});

imageQueue.process(function (job, done) {
  // transcode image asynchronously and report progress
  job.progress(42);

  // call done when finished
  done();

  // or give an error if error
  done(new Error('error transcoding'));

  // or pass it a result
  done(null, { width: 1280, height: 720 /* etc... */ });

  // If the job throws an unhandled exception it is also handled correctly
  throw new Error('some unexpected error');
});

pdfQueue.process(function (job) {
  // Processors can also return promises instead of using the done callback
  return pdfAsyncProcessor();
});

videoQueue.add({ video: 'http://example.com/video1.mov' });
audioQueue.add({ audio: 'http://example.com/audio1.mp3' });
imageQueue.add({ image: 'http://example.com/image1.tiff' });

Using promises

Alternatively, you can return promises instead of using the done callback:

videoQueue.process(function (job) { // don't forget to remove the done callback!
  // Simply return a promise
  return fetchVideo(job.data.url).then(transcodeVideo);

  // Handles promise rejection
  return Promise.reject(new Error('error transcoding'));

  // Passes the value the promise is resolved with to the "completed" event
  return Promise.resolve({ framerate: 29.5 /* etc... */ });

  // If the job throws an unhandled exception it is also handled correctly
  throw new Error('some unexpected error');
  // same as
  return Promise.reject(new Error('some unexpected error'));
});

Separate processes

The process function can also be run in a separate process. This has several advantages:

  • The process is sandboxed so if it crashes it does not affect the worker.
  • You can run blocking code without affecting the queue (jobs will not stall).
  • Much better utilization of multi-core CPUs.
  • Less connections to redis.

In order to use this feature just create a separate file with the processor:

// processor.js
module.exports = function (job) {
  // Do some heavy work

  return Promise.resolve(result);
}

And define the processor like this:

// Single process:
queue.process('/path/to/my/processor.js');

// You can use concurrency as well:
queue.process(5, '/path/to/my/processor.js');

// and named processors:
queue.process('my processor', 5, '/path/to/my/processor.js');

Repeated jobs

A job can be added to a queue and processed repeatedly according to a cron specification:

  paymentsQueue.process(function (job) {
    // Check payments
  });

  // Repeat payment job once every day at 3:15 (am)
  paymentsQueue.add(paymentsData, { repeat: { cron: '15 3 * * *' } });

As a tip, check your expressions here to verify they are correct: cron expression generator

Pause / Resume

A queue can be paused and resumed globally (pass true to pause processing for just this worker):

queue.pause().then(function () {
  // queue is paused now
});

queue.resume().then(function () {
  // queue is resumed now
})

Events

A queue emits some useful events, for example...

.on('completed', function (job, result) {
  // Job completed with output result!
})

For more information on events, including the full list of events that are fired, check out the Events reference

Queues performance

Queues are cheap, so if you need many of them just create new ones with different names:

const userJohn = new Queue('john');
const userLisa = new Queue('lisa');
.
.
.

However every queue instance will require new redis connections, check how to reuse connections or you can also use named processors to achieve a similar result.

Cluster support

NOTE: From version 3.2.0 and above it is recommended to use threaded processors instead.

Queues are robust and can be run in parallel in several threads or processes without any risk of hazards or queue corruption. Check this simple example using cluster to parallelize jobs across processes:

const Queue = require('bull');
const cluster = require('cluster');

const numWorkers = 8;
const queue = new Queue('test concurrent queue');

if (cluster.isMaster) {
  for (let i = 0; i < numWorkers; i++) {
    cluster.fork();
  }

  cluster.on('online', function (worker) {
    // Let's create a few jobs for the queue workers
    for (let i = 0; i < 500; i++) {
      queue.add({ foo: 'bar' });
    };
  });

  cluster.on('exit', function (worker, code, signal) {
    console.log('worker ' + worker.process.pid + ' died');
  });
} else {
  queue.process(function (job, jobDone) {
    console.log('Job done by worker', cluster.worker.id, job.id);
    jobDone();
  });
}

Documentation

For the full documentation, check out the reference and common patterns:

  • Guide — Your starting point for developing with Bull.
  • Reference — Reference document with all objects and methods available.
  • Patterns — a set of examples for common patterns.
  • License — the Bull license—it's MIT.

If you see anything that could use more docs, please submit a pull request!


Important Notes

The queue aims for an "at least once" working strategy. This means that in some situations, a job could be processed more than once. This mostly happens when a worker fails to keep a lock for a given job during the total duration of the processing.

When a worker is processing a job it will keep the job "locked" so other workers can't process it.

It's important to understand how locking works to prevent your jobs from losing their lock - becoming stalled - and being restarted as a result. Locking is implemented internally by creating a lock for lockDuration on interval lockRenewTime (which is usually half lockDuration). If lockDuration elapses before the lock can be renewed, the job will be considered stalled and is automatically restarted; it will be double processed. This can happen when:

  1. The Node process running your job processor unexpectedly terminates.
  2. Your job processor was too CPU-intensive and stalled the Node event loop, and as a result, Bull couldn't renew the job lock (see #488 for how we might better detect this). You can fix this by breaking your job processor into smaller parts so that no single part can block the Node event loop. Alternatively, you can pass a larger value for the lockDuration setting (with the tradeoff being that it will take longer to recognize a real stalled job).

As such, you should always listen for the stalled event and log this to your error monitoring system, as this means your jobs are likely getting double-processed.

As a safeguard so problematic jobs won't get restarted indefinitely (e.g. if the job processor always crashes its Node process), jobs will be recovered from a stalled state a maximum of maxStalledCount times (default: 1).