agenda vs cron vs later vs node-cron vs node-schedule
Job Scheduling Libraries in Node.js
agendacronlaternode-cronnode-scheduleSimilar Packages:

Job Scheduling Libraries in Node.js

agenda, cron, later, node-cron, and node-schedule are all Node.js libraries designed to schedule and execute tasks at specific times or intervals. These packages help developers automate background jobs, recurring operations, or time-based workflows without relying on external systems like cron daemons. While they share the common goal of time-based task execution, they differ significantly in architecture, persistence, syntax, and use cases β€” ranging from simple in-memory timers to database-backed job queues with retry logic and concurrency control.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
agenda09,651295 kB2a month agoMIT
cron08,915161 kB254 months agoMIT
later02,418-9810 years agoMIT
node-cron03,248221 kB339 months agoISC
node-schedule09,22135 kB1713 years agoMIT

Job Scheduling in Node.js: agenda vs cron vs later vs node-cron vs node-schedule

Scheduling tasks in Node.js seems simple at first β€” just use setTimeout or setInterval, right? But real-world apps often need more: cron-like expressions, job persistence, error recovery, or complex recurrence rules. That’s where dedicated scheduling libraries come in. Let’s compare five popular options to see which fits your architecture.

πŸ—ƒοΈ Persistence & Reliability: In-Memory vs Database-Backed

Only agenda stores jobs in a database (MongoDB). All others run entirely in memory, meaning scheduled jobs vanish if your process crashes or restarts.

agenda uses MongoDB to track job status, retries, and next run times. This makes it suitable for critical background work like sending emails or processing payments.

// agenda: Jobs survive restarts
const agenda = new Agenda({ db: { address: 'mongodb://localhost/jobs' } });

agenda.define('send email', async (job) => {
  await sendEmail(job.attrs.data.to);
});

await agenda.start();
agenda.schedule('in 10 minutes', 'send email', { to: 'user@example.com' });

In contrast, cron, node-cron, node-schedule, and later keep schedules in memory only:

// cron: Lost on restart
const CronJob = require('cron').CronJob;
new CronJob('0 */5 * * * *', () => {
  console.log('Runs every 5 minutes β€” but not if server restarted');
}, null, true);

// node-cron
const cron = require('node-cron');
cron.schedule('*/5 * * * *', () => {
  console.log('Same limitation');
});

// node-schedule
const schedule = require('node-schedule');
schedule.scheduleJob('*/5 * * * *', () => {
  console.log('Also in-memory only');
});

// later: doesn't run jobs at all β€” just calculates times
const later = require('later');
later.parse.recur().every(5).minute(); // returns a schedule object

πŸ’‘ If your app can’t afford to lose scheduled work, agenda is the only option here. Otherwise, in-memory schedulers are fine for non-critical tasks.

⏱️ Syntax & Expressiveness: Cron Strings vs Natural Language

All libraries support cron-style expressions, but with key differences in precision and flexibility.

cron, node-cron, and node-schedule accept standard cron strings (5 or 6 fields). node-cron and cron support a 6th field for seconds:

// cron (6 fields: seconds optional)
new CronJob('10 * * * * *', fn); // every minute at 10 seconds

// node-cron (6 fields required)
nodeCron.schedule('10 * * * * *', fn);

// node-schedule (5 fields by default, but supports seconds via object)
schedule.scheduleJob({ hour: 10, minute: 30, second: 0 }, fn);

later stands out by offering a fluent API for building complex schedules:

// later: Every weekday at 9:30 AM
const sched = later.parse.text('at 9:30am on weekdays');
// Or programmatically
const sched = later.parse.recur().on(9, 30).time().onWeekday();

However, later doesn’t execute jobs β€” you must use setTimeout or similar with its output:

const next = later.schedule(sched).next(1);
setTimeout(fn, next.getTime() - Date.now());

agenda supports both cron strings and human-readable text (via human-interval):

agenda.schedule('tomorrow at 4pm', 'task');
agenda.schedule('*/5 * * * *', 'task');

πŸ” Job Execution Model: Callbacks, Promises, and Concurrency

agenda is the only library that manages job concurrency, retries, and failure handling:

agenda.define('process file', { concurrency: 5, retry: true }, async (job) => {
  // Agenda handles errors, retries, and limits concurrent jobs
});

The others execute jobs as plain functions with no built-in error isolation:

// cron
new CronJob('* * * * * *', () => {
  throw new Error('Crashes the whole process!');
});

// node-cron
cron.schedule('* * * * * *', () => {
  // Unhandled rejections may terminate the process
});

// node-schedule
schedule.scheduleJob('* * * * *', () => {
  // Same risk
});

You must wrap job logic in try/catch or .catch() when using non-agenda libraries to prevent crashes.

🧩 Real-World Use Cases

Case 1: Sending Daily Reports

You need to email a report every day at 8 AM, even if the server restarts overnight.

  • βœ… Best choice: agenda β€” persistent, retryable, and survives restarts.
  • ❌ Others: Jobs disappear on restart; no guarantee delivery.

Case 2: Polling an External API Every 30 Seconds

A short-lived script that fetches data repeatedly while running.

  • βœ… Best choice: node-cron or cron β€” lightweight, second-level precision.
  • ❌ agenda: Overkill; requires MongoDB.
  • ❌ later: Doesn’t run jobs.

Case 3: Running a Cleanup Task on the Last Friday of Each Month

Complex recurrence rule needed.

  • βœ… Best choice: later + setTimeout for schedule definition, or agenda if persistence matters.
  • ❌ cron/node-cron: Can’t express "last Friday" easily in cron syntax.

Case 4: One-Time Notification in 2 Hours

Schedule a single future action.

  • βœ… Best choice: node-schedule β€” clean support for one-off jobs:
    schedule.scheduleJob(new Date(Date.now() + 7200000), fn);
    
  • βœ… Also works in agenda, but heavier.
  • ❌ cron/node-cron: Designed for recurring jobs; awkward for one-time.

🚫 Deprecated or Problematic?

As of 2024, none of these packages are officially deprecated. However:

  • later hasn’t seen significant updates in years and is best viewed as a schedule parser, not a job runner.
  • cron and node-cron are both actively maintained but serve nearly identical purposes β€” prefer node-cron for its cleaner API and explicit second support.
  • agenda remains the go-to for persistent job queues but ties you to MongoDB.
  • node-schedule is stable and widely used for in-memory scheduling with flexible input formats.

πŸ“Š Summary Table

Featureagendacronlaternode-cronnode-schedule
Persistenceβœ… MongoDB❌❌❌❌
Cron Syntaxβœ…βœ… (5/6 fields)❌ (custom)βœ… (6 fields)βœ… (5 fields)
Human-Readableβœ…βŒβœ…βŒβœ… (limited)
One-Time Jobsβœ…βš οΈ (awkward)βœ… (with DIY)βš οΈβœ…
Concurrency Controlβœ…βŒβŒβŒβŒ
Error Handlingβœ… Built-in❌ Manual❌ N/A❌ Manual❌ Manual
External DepsMongoDBNoneNoneNoneNone

πŸ’‘ Final Recommendation

  • Need reliability and persistence? β†’ agenda
  • Need simple, recurring in-memory jobs? β†’ node-cron (or cron if you prefer it)
  • Need complex recurrence rules? β†’ later (as a helper) + your own runner, or agenda if persistent
  • Need flexible one-time or recurring jobs without deps? β†’ node-schedule

Choose based on whether your jobs can afford to disappear β€” if yes, keep it simple; if no, go with agenda.

How to Choose: agenda vs cron vs later vs node-cron vs node-schedule

  • agenda:

    Choose agenda if you need a persistent, MongoDB-backed job queue with features like job retries, concurrency control, and failure handling. It’s ideal for production applications requiring reliability, auditability, and the ability to resume scheduled jobs after server restarts. Avoid it if you don’t already use MongoDB or need lightweight, in-memory scheduling.

  • cron:

    Choose cron if you want a minimal, battle-tested library that mimics Unix cron syntax and runs jobs in memory. It’s well-suited for simple, recurring tasks in long-running processes where persistence isn’t required. Its clean API and support for second-level precision make it a solid choice for basic scheduling needs without external dependencies.

  • later:

    Choose later if you need advanced, human-readable recurrence rules (e.g., 'every 2nd Tuesday of the month') and plan to generate schedules programmatically. However, note that later is primarily a schedule parser and does not execute jobs by itself β€” you must integrate it with your own timer logic. It’s best used as a utility within custom schedulers rather than a standalone solution.

  • node-cron:

    Choose node-cron if you prefer a straightforward, cron-like syntax with second-level granularity and don’t need job persistence. It’s similar to cron but offers a slightly more modern API and active maintenance. Ideal for internal tooling, scripts, or services where jobs can be lost on process restart and that’s acceptable.

  • node-schedule:

    Choose node-schedule if you need flexible scheduling using either cron-style strings or JavaScript Date objects, and want built-in support for one-time and recurring jobs without external storage. It’s a good middle ground for applications that require more expressiveness than basic intervals but don’t justify a full job queue system.

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