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.
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.
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,
agendais the only option here. Otherwise, in-memory schedulers are fine for non-critical tasks.
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');
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.
You need to email a report every day at 8 AM, even if the server restarts overnight.
agenda β persistent, retryable, and survives restarts.A short-lived script that fetches data repeatedly while running.
node-cron or cron β lightweight, second-level precision.agenda: Overkill; requires MongoDB.later: Doesnβt run jobs.Complex recurrence rule needed.
later + setTimeout for schedule definition, or agenda if persistence matters.cron/node-cron: Canβt express "last Friday" easily in cron syntax.Schedule a single future action.
node-schedule β clean support for one-off jobs:
schedule.scheduleJob(new Date(Date.now() + 7200000), fn);
agenda, but heavier.cron/node-cron: Designed for recurring jobs; awkward for one-time.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.| Feature | agenda | cron | later | node-cron | node-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 Deps | MongoDB | None | None | None | None |
agendanode-cron (or cron if you prefer it)later (as a helper) + your own runner, or agenda if persistentnode-scheduleChoose based on whether your jobs can afford to disappear β if yes, keep it simple; if no, go with 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.
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.
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.
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.
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.
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