agenda vs bull vs kue vs pg-boss
Background Job Queues for Node.js Applications
agendabullkuepg-bossSimilar Packages:

Background Job Queues for Node.js Applications

agenda, bull, kue, and pg-boss are Node.js libraries designed to manage background tasks and scheduled jobs. They allow developers to offload time-consuming work β€” such as sending emails, processing images, or generating reports β€” from the main application thread. Each library uses a different storage backend: agenda relies on MongoDB, bull and kue use Redis, and pg-boss leverages PostgreSQL. These tools help ensure tasks are not lost during crashes and can be retried if they fail.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
agenda09,642295 kB225 days agoMIT
bull016,241309 kB146a year agoMIT
kue09,469-2889 years agoMIT
pg-boss03,291268 kB3814 days agoMIT

Background Job Queues: Agenda vs Bull vs Kue vs pg-boss

When building Node.js applications, you often need to handle tasks that take too long to run during a standard HTTP request. Sending emails, resizing images, or syncing data are common examples. The agenda, bull, kue, and pg-boss packages solve this by moving work to background jobs. They ensure tasks are saved safely and processed reliably, even if the server restarts. Let's compare how they handle storage, job definition, scheduling, and error management.

πŸ’Ύ Storage Engine: MongoDB vs Redis vs PostgreSQL

The core difference lies in where job data is stored. This affects your infrastructure choices and performance.

agenda stores jobs directly in MongoDB collections.

  • Uses standard database documents for job state.
  • Good if you already use Mongo for app data.
// agenda: MongoDB connection
const agenda = new Agenda({ db: { address: 'mongodb://localhost/agenda' } });
await agenda.start();

bull stores jobs in Redis using lists and hashes.

  • Very fast read/write operations.
  • Requires a separate Redis server instance.
// bull: Redis connection
const Queue = require('bull');
const queue = new Queue('email-queue', 'redis://localhost:6379');

kue also stores jobs in Redis.

  • Similar to bull but uses an older data structure approach.
  • Requires a separate Redis server instance.
// kue: Redis connection
const kue = require('kue');
const queue = kue.createQueue({ redis: { port: 6379, host: 'localhost' } });

pg-boss stores jobs in PostgreSQL tables.

  • Uses standard SQL tables and LISTEN/NOTIFY.
  • No new database engine needed if you use Postgres.
// pg-boss: PostgreSQL connection
const Boss = require('pg-boss');
const boss = new Boss({ connectionString: 'postgres://user:pass@localhost/db' });
await boss.start();

πŸ“ Defining and Processing Jobs

Each library has a different way to register the function that actually does the work.

agenda uses define to register a handler.

  • The handler receives a job object with data.
  • Uses async/await or callbacks.
// agenda: Define job handler
agenda.define('send-email', async (job) => {
  const { to, subject } = job.attrs.data;
  await sendMail(to, subject);
});

bull uses process to register a worker.

  • The worker function receives the job data directly.
  • Supports concurrency limits.
// bull: Process job
queue.process(async (job) => {
  const { to, subject } = job.data;
  await sendMail(to, subject);
});

kue uses process with a callback style.

  • Requires calling done() to signal completion.
  • Older pattern compared to modern promises.
// kue: Process job
queue.process('send-email', async (job, done) => {
  try {
    await sendMail(job.data.to, job.data.subject);
    done();
  } catch (err) {
    done(err);
  }
});

pg-boss uses work to register a handler.

  • Similar to agenda but with Postgres backend.
  • Returns a promise to signal completion.
// pg-boss: Work on job
await boss.work('send-email', async (job) => {
  const { to, subject } = job.data;
  await sendMail(to, subject);
});

πŸ•’ Scheduling and Immediate Execution

Some tasks need to run right away, while others need to wait for a specific time.

agenda supports now for immediate jobs and schedule for future jobs.

  • Very flexible cron syntax for recurring tasks.
// agenda: Schedule job
await agenda.now('send-email', { to: 'user@example.com' });
await agenda.schedule('in 1 hour', 'send-email', { to: 'user@example.com' });

bull uses add with options for delays.

  • Supports delayed jobs via Redis timestamps.
  • Recurring jobs require plugins or external schedulers.
// bull: Add delayed job
await queue.add({ to: 'user@example.com' }, { delay: 3600000 });

kue uses delay on the job instance.

  • Simple delay mechanism.
  • Less robust recurring job support.
// kue: Delay job
const job = queue.create('send-email', { to: 'user@example.com' });
job.delay(3600000).save();

pg-boss uses send with a startAfter option.

  • Uses database timestamps for scheduling.
  • Supports cron-like scheduling via configuration.
// pg-boss: Send scheduled job
await boss.send('send-email', { to: 'user@example.com' }, { startAfter: '1 hour' });

⚠️ Error Handling and Retries

Jobs fail. Networks drop. Databases lock. Your queue must handle these issues.

agenda retries based on configuration.

  • Failed jobs remain in the database for inspection.
  • You must manually handle failure logic in the define block.
// agenda: Error handling
agenda.define('send-email', async (job) => {
  try {
    await sendMail();
  } catch (err) {
    // Log error, job remains failed in DB
    console.error(err);
  }
});

bull has built-in retry attempts.

  • Configure attempts and backoff strategies easily.
  • Moves failed jobs to a special failed list.
// bull: Retry configuration
queue.process(async (job) => {
  await sendMail();
}, { attempts: 3, backoff: { type: 'exponential', delay: 1000 } });

kue handles retries via attempts.

  • Similar to bull but less configurable.
  • Failed jobs stay in the queue UI for review.
// kue: Retry configuration
const job = queue.create('send-email', data);
job.attempts(3).save();

pg-boss manages retries internally.

  • Expired or failed jobs can be archived or retried.
  • Uses database transactions for safety.
// pg-boss: Retry configuration
await boss.work('send-email', { retryLimit: 3 }, async (job) => {
  await sendMail();
});

πŸ›‘ Maintenance Status and Risks

Choosing a library means trusting its future support.

agenda is actively maintained.

  • Regular updates and security patches.
  • Safe for long-term projects.

bull is in maintenance mode.

  • Still stable, but bullmq is the recommended modern version.
  • Safe for existing projects, consider bullmq for new ones.

kue is deprecated and abandoned.

  • Do not use for new projects.
  • Known security issues and no fixes.

pg-boss is actively maintained.

  • Growing popularity among Postgres users.
  • Safe for long-term projects.

πŸ“Š Summary: Key Differences

Featureagendabullkuepg-boss
BackendπŸƒ MongoDBπŸ“ RedisπŸ“ Redis🐘 PostgreSQL
Statusβœ… Active⚠️ Maintenance❌ Deprecatedβœ… Active
SchedulingπŸ•’ Built-in Cron⏱️ Delay Only⏱️ Delay OnlyπŸ•’ Built-in Cron
API StyleπŸ“˜ Define/NowπŸ“˜ Add/ProcessπŸ“˜ Create/ProcessπŸ“˜ Publish/Work
InfrastructureπŸ—„οΈ Requires MongoπŸ—„οΈ Requires RedisπŸ—„οΈ Requires RedisπŸ—„οΈ Requires Postgres

πŸ’‘ Final Recommendation

agenda is the best choice if you already run MongoDB and need complex scheduling without adding new tools. It balances features and simplicity well.

bull remains a solid option for high-performance needs with Redis, but consider its successor bullmq for new development. It handles high concurrency better than most.

kue should be avoided entirely. It is outdated and poses security risks. Migrate existing systems to bull or pg-boss.

pg-boss is the top pick for PostgreSQL shops. It reduces infrastructure complexity by keeping jobs in your main database. It is modern, safe, and easy to adopt.

Final Thought: All four libraries solve the same problem β€” keeping background work reliable. Your choice should depend on your existing database stack and whether you need advanced scheduling features.

How to Choose: agenda vs bull vs kue vs pg-boss

  • agenda:

    Choose agenda if your infrastructure already relies on MongoDB and you need robust cron-like scheduling features. It is well-suited for applications that require complex job recurrence rules without adding new database dependencies. However, be aware that heavy write loads on the job collection can impact database performance.

  • bull:

    Choose bull if you need high-performance job processing and already use Redis for caching or sessions. It is a mature solution with strong community support, though note that bullmq is the newer successor for modern projects. This package excels in scenarios requiring priority queues and high concurrency.

  • kue:

    Do not choose kue for new projects as it is deprecated and no longer maintained. While it pioneered Redis-based queues in Node.js, security vulnerabilities and lack of updates make it risky. Evaluate bull or pg-boss instead for similar functionality with active support.

  • pg-boss:

    Choose pg-boss if your stack is centered on PostgreSQL and you want to avoid managing separate Redis or MongoDB instances. It uses standard database features like LISTEN/NOTIFY to handle job distribution efficiently. This is ideal for teams seeking to simplify infrastructure by consolidating data storage.

README for agenda

Agenda

Agenda

A light-weight job scheduling library for Node.js

NPM Version NPM Downloads

Migrating from v5? See the Migration Guide for all breaking changes.

Agenda 6.x

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.

Features

  • Minimal overhead job scheduling
  • Pluggable storage backends (MongoDB, PostgreSQL, Redis)
  • TypeScript support with full typing
  • Scheduling via cron or human-readable syntax
  • Configurable concurrency and locking
  • Real-time job notifications (optional)
  • Sandboxed worker execution via fork mode
  • TypeScript decorators for class-based job definitions

Installation

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:

  • Node.js 18+
  • Database of your choice (MongoDB 4+, PostgreSQL, or Redis)

Quick Start

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' });

Official Backend Packages

PackageBackendNotificationsInstall
@agendajs/mongo-backendMongoDBPolling onlynpm install @agendajs/mongo-backend
@agendajs/postgres-backendPostgreSQLLISTEN/NOTIFYnpm install @agendajs/postgres-backend
@agendajs/redis-backendRedisPub/Subnpm install @agendajs/redis-backend

Backend Capabilities

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

Backend Configuration

MongoDB

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
});

PostgreSQL

import { Agenda } from 'agenda';
import { PostgresBackend } from '@agendajs/postgres-backend';

const agenda = new Agenda({
  backend: new PostgresBackend({
    connectionString: 'postgresql://user:pass@localhost:5432/mydb'
  })
});

Redis

import { Agenda } from 'agenda';
import { RedisBackend } from '@agendajs/redis-backend';

const agenda = new Agenda({
  backend: new RedisBackend({
    connectionString: 'redis://localhost:6379'
  })
});

Real-Time Notifications

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()
});

Mixing Storage and Notification Backends

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.

API Overview

Defining Jobs

// 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'
});

Defining Jobs with Decorators

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.

Scheduling Jobs

// 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

Job Control

// 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
  }
});

Stopping / Draining

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

Events

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) => { /* ... */ });

Custom Backend

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.

Documentation

Related Packages

Official Backend Packages:

Tools:

License

MIT