bull vs agenda vs graphile-worker
Job Queue Libraries for Node.js Comparison
1 Year
bullagendagraphile-workerSimilar Packages:
What's Job Queue Libraries for Node.js?

Job queue libraries are essential for managing background tasks in Node.js applications. They allow developers to offload time-consuming tasks from the main thread, ensuring that the application remains responsive. These libraries provide mechanisms for scheduling jobs, handling retries, and managing concurrency, making them invaluable for applications that require efficient task processing. Each library has its unique features and design principles, catering to different use cases and preferences in job management.

Package Weekly Downloads Trend
Github Stars Ranking
Stat Detail
Package
Downloads
Stars
Size
Issues
Publish
License
bull996,52016,020309 kB1456 months agoMIT
agenda141,1929,537353 kB353-MIT
graphile-worker53,5201,979499 kB29a year agoMIT
Feature Comparison: bull vs agenda vs graphile-worker

Storage Backend

  • bull:

    Bull utilizes Redis as its storage backend, which is known for its speed and efficiency in handling job queues. Redis allows for fast access to job data and supports advanced features like job retries and delayed jobs, making Bull suitable for high-throughput applications.

  • agenda:

    Agenda uses MongoDB as its storage backend, allowing for easy integration with applications that already utilize MongoDB. It stores jobs as documents in a collection, providing a simple way to query and manage jobs.

  • graphile-worker:

    Graphile Worker uses PostgreSQL as its backend, leveraging its relational capabilities to manage job data. This allows for complex job relationships and the ability to utilize SQL queries for job management, making it a powerful choice for applications heavily reliant on PostgreSQL.

Job Scheduling

  • bull:

    Bull offers advanced job scheduling capabilities, including delayed jobs and job prioritization. You can easily manage when jobs should execute and their processing order, making it ideal for applications with varying job priorities.

  • agenda:

    Agenda provides a straightforward API for scheduling jobs, including support for recurring jobs using cron syntax. This makes it easy to set up jobs that need to run at specific intervals, simplifying the scheduling process.

  • graphile-worker:

    Graphile Worker allows for job scheduling through PostgreSQL, enabling you to define jobs with specific execution times. It leverages PostgreSQL's capabilities to manage job states and dependencies effectively.

Performance

  • bull:

    Bull is designed for performance, utilizing Redis to handle a large number of jobs efficiently. It supports concurrency and can process multiple jobs simultaneously, making it a great choice for applications that require high throughput.

  • agenda:

    While Agenda is suitable for many applications, its performance can be limited by MongoDB's capabilities, especially under heavy load or with a large number of jobs. It may not be the best choice for high-performance scenarios where speed is critical.

  • graphile-worker:

    Graphile Worker benefits from PostgreSQL's performance optimizations, allowing for efficient job processing. It can handle complex job relationships and workflows, making it suitable for applications with intricate job dependencies.

Ease of Use

  • bull:

    Bull has a more complex API due to its advanced features, but it provides extensive documentation and examples to help developers understand its capabilities. The learning curve may be steeper for those unfamiliar with Redis.

  • agenda:

    Agenda has a simple and intuitive API, making it easy to get started with job scheduling. Its integration with Mongoose also simplifies the setup for developers familiar with MongoDB.

  • graphile-worker:

    Graphile Worker is designed to be straightforward for developers already using PostgreSQL. Its integration with SQL makes it easy to manage jobs, but it may require a deeper understanding of relational databases for optimal use.

Community and Support

  • bull:

    Bull has a large and active community, providing extensive resources, documentation, and support. Its popularity means that developers can find solutions to common problems more easily.

  • agenda:

    Agenda has a smaller community compared to Bull and Graphile Worker, which may result in less community support and fewer resources available for troubleshooting and learning.

  • graphile-worker:

    Graphile Worker is part of the Graphile ecosystem, which has a growing community. While it may not be as large as Bull's, it benefits from the support of PostgreSQL enthusiasts and developers focused on performance.

How to Choose: bull vs agenda vs graphile-worker
  • bull:

    Choose Bull if you require a robust, Redis-based job queue that supports advanced features like rate limiting, job prioritization, and delayed jobs. Bull is suitable for high-performance applications that need to handle a large volume of jobs efficiently and offers a rich set of features for job management.

  • agenda:

    Choose Agenda if you need a simple, MongoDB-backed job scheduler that integrates seamlessly with Mongoose and allows for recurring jobs with a straightforward API. It's ideal for applications that already use MongoDB and require basic job scheduling capabilities.

  • graphile-worker:

    Choose Graphile Worker if you are looking for a job queue that integrates tightly with PostgreSQL and leverages its capabilities for job management. It's particularly useful for applications that already use PostgreSQL and require a job queue that can handle complex workflows and relationships.

README for bull



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

  • [x] Minimal CPU usage due to a polling-free design.
  • [x] Robust design based on Redis.
  • [x] Delayed jobs.
  • [x] Schedule and repeat jobs according to a cron specification.
  • [x] Rate limiter for jobs.
  • [x] Retries.
  • [x] Priority.
  • [x] Concurrency.
  • [x] Pause/resume—globally or locally.
  • [x] Multiple job types per queue.
  • [x] Threaded (sandboxed) processing functions.
  • [x] 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:

| Feature | BullMQ-Pro | BullMQ | Bull | Kue | Bee | Agenda | | :------------------------ | :-------------: | :-------------: | :-------------: | :---: | -------- | ------ | | Backend | redis | redis | redis | redis | redis | mongo | | 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 for | Jobs / Messages | Jobs / Messages | Jobs / Messages | Jobs | Messages | Jobs |

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).