agenda, cron, later, and node-cron are all npm packages designed to schedule and execute recurring or delayed tasks in Node.js applications. They enable developers to run functions at specific times, on intervals, or according to complex schedules—commonly used for background jobs like sending emails, cleaning up data, or syncing external services. While they share the goal of time-based task execution, they differ significantly in architecture, persistence, scheduling syntax, and runtime requirements.
When you need to run code at specific times—like sending a daily report or cleaning up stale sessions—you’ll likely reach for a job scheduler. The four packages compared here (agenda, cron, later, node-cron) all solve this problem, but with very different trade-offs around persistence, syntax, maintenance status, and runtime behavior. Let’s break down how they work in practice.
cron and node-cron both use standard cron syntax (e.g., '0 2 * * *' for “2 AM every day”). This is familiar to Unix users but can be hard to read for complex schedules.
// cron
const CronJob = require('cron').CronJob;
new CronJob('0 2 * * *', () => {
console.log('Run at 2 AM');
}, null, true);
// node-cron
const cron = require('node-cron');
cron.schedule('0 2 * * *', () => {
console.log('Run at 2 AM');
});
later uses a fluent, programmatic API to build schedules in plain JavaScript, making it more readable for non-cron experts.
// later (deprecated — do not use in new projects)
const later = require('later');
later.date.localTime();
const sched = later.parse.text('at 2:00 am');
later.setInterval(() => {
console.log('Run at 2 AM');
}, sched);
agenda doesn’t use cron syntax directly. Instead, it accepts either cron strings or human-readable intervals like '3 minutes'.
// agenda
const agenda = new Agenda({ db: { address: 'mongodb://...' } });
agenda.define('daily report', async (job) => {
console.log('Generating report');
});
await agenda.start();
await agenda.every('0 2 * * *', 'daily report'); // cron style
// or: await agenda.every('24 hours', 'daily report');
⚠️ Important:
lateris officially deprecated. Its npm page states: “This project is no longer maintained.” Avoid it in new code.
cron, node-cron, and later are in-memory schedulers. If your Node.js process crashes or restarts, scheduled jobs are lost. They’re fine for ephemeral tasks but risky for critical workflows.
agenda stores jobs in MongoDB, so schedules survive restarts. It also tracks job status (pending, running, failed), supports retries, and allows querying jobs via the database.
// With agenda, even after a restart:
await agenda.start();
// All previously scheduled jobs resume automatically
This makes agenda suitable for production systems where job reliability matters—like processing payments or sending user emails.
cron and node-cron are self-contained. They run entirely within your Node.js process using setTimeout/setInterval under the hood. No external services needed.
agenda requires a MongoDB connection. This adds operational complexity but enables features like distributed job locking (so multiple app instances don’t run the same job twice).
later is also self-contained but unmaintained.
Only agenda provides full job lifecycle management:
// agenda: rich job control
agenda.define('send email', { priority: 'high', concurrency: 10 }, async (job) => {
await sendEmail(job.attrs.data.to, job.attrs.data.subject);
});
await agenda.now('send email', { to: 'user@example.com', subject: 'Welcome!' });
In contrast, the other packages only support fire-and-forget callbacks:
// node-cron: no job context or data
cron.schedule('* * * * *', () => {
// No access to job metadata or input
doWork();
});
cron and node-cron will silently swallow unhandled errors in job callbacks unless you wrap them:
// node-cron: must handle errors manually
cron.schedule('* * * * *', () => {
try {
riskyOperation();
} catch (err) {
logger.error(err);
}
});
agenda catches errors automatically and marks jobs as failed. You can listen for failures:
agenda.on('fail', (err, job) => {
console.error(`Job ${job.attrs.name} failed:`, err);
});
agendanode-cron or croncron or node-cron with '0 9,15 * * 1-5'later due to deprecation.agenda| Feature | agenda | cron | later | node-cron |
|---|---|---|---|---|
| Persistence | ✅ MongoDB | ❌ In-memory | ❌ In-memory | ❌ In-memory |
| Cron Syntax | ✅ (plus intervals) | ✅ | ❌ (uses text parsing) | ✅ |
| Maintenance Status | ✅ Actively maintained | ✅ Actively maintained | ❌ Deprecated | ✅ Actively maintained |
| Job Retries | ✅ Built-in | ❌ Manual | ❌ Manual | ❌ Manual |
| External Dep | ✅ MongoDB | ❌ None | ❌ None | ❌ None |
| Concurrency Control | ✅ Per job type | ❌ None | ❌ None | ❌ None |
agenda (accept the MongoDB dependency).node-cron (cleaner API) or cron (more mature).later? → Don’t. It’s deprecated. Rewrite schedules using cron syntax instead.For most frontend teams building Node.js services, node-cron strikes the best balance of simplicity and functionality—unless you truly need durable jobs, in which case agenda is worth the extra setup.
Choose agenda if you need persistent, MongoDB-backed job scheduling with features like job retries, priorities, and concurrency control. It’s ideal for production systems where job durability and visibility matter—such as processing user notifications or batch exports—but requires a MongoDB instance and is heavier than in-memory alternatives.
Choose cron if you want a lightweight, battle-tested scheduler that supports standard cron syntax and runs entirely in memory. It’s well-suited for simple, time-based callbacks in scripts or services where job persistence isn’t required, but avoid it if you need guaranteed execution after crashes or complex recurrence rules beyond cron expressions.
Choose later if you need advanced, human-readable scheduling logic (e.g., 'every 2 hours between 9 AM and 5 PM on weekdays') without relying on cron syntax. However, note that later is deprecated and no longer maintained—do not use it in new projects. Existing users should migrate to alternatives like cron or node-cron.
Choose node-cron if you prefer a minimal, zero-dependency scheduler that mimics Unix cron behavior with a clean API. It’s great for straightforward cron-style tasks in environments where simplicity and small footprint matter, but lacks job persistence, retry mechanisms, or advanced scheduling beyond standard cron fields.
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