Scheduling
Schedule recurring tasks in Node.js using node-cron for simple cron jobs and Agenda.js for persistent, database-backed job queues with retry logic and graceful shutdown.
Cron Expression Cheat Sheet
Cron expressions use 5 (or 6) fields to describe when a job fires. Node-cron supports an optional seconds field as the first position.
┌─────────── second (0-59) [optional, node-cron only]
│ ┌───────── minute (0-59)
│ │ ┌─────── hour (0-23)
│ │ │ ┌───── day of month (1-31)
│ │ │ │ ┌─── month (1-12 or JAN-DEC)
│ │ │ │ │ ┌─ day of week (0-7 or SUN-SAT, 0 and 7 = Sunday)
│ │ │ │ │ │
* * * * * *
Examples:
'* * * * *' – every minute
'0 * * * *' – every hour (at :00)
'0 0 * * *' – every day at midnight
'0 9 * * 1-5' – weekdays at 9:00 AM
'0 0 1 * *' – first day of every month at midnight
'0 */6 * * *' – every 6 hours
'30 8 * * 1' – every Monday at 08:30
'0 0 * * 0' – every Sunday at midnightnode-cron Setup & Basic Jobs
Simple, lightweight cron scheduling that runs in-process. Jobs share the same Node.js process and memory as your app server.
npm install node-cron
// scheduler/index.js
const cron = require('node-cron');
const db = require('../db');
const mailer = require('../lib/mailer');
const tasks = [];
function startScheduler() {
// Run every day at 01:00 AM – purge expired sessions
const purgeTask = cron.schedule('0 1 * * *', async () => {
console.log('[CRON] purging expired sessions...');
try {
const [result] = await db.query(
'DELETE FROM sessions WHERE expires_at < NOW()'
);
console.log(`[CRON] deleted ${result.affectedRows} sessions`);
} catch (err) {
console.error('[CRON] purge failed:', err.message);
}
}, { timezone: 'Asia/Jakarta' });
// Every Monday at 08:00 – send weekly digest emails
const digestTask = cron.schedule('0 8 * * 1', async () => {
console.log('[CRON] sending weekly digest...');
try {
const [users] = await db.query('SELECT * FROM users WHERE digest_opt_in = 1');
for (const user of users) {
await mailer.sendDigest(user);
}
} catch (err) {
console.error('[CRON] digest failed:', err.message);
}
}, { timezone: 'Asia/Jakarta' });
tasks.push(purgeTask, digestTask);
console.log('[CRON] scheduler started');
}
function stopScheduler() {
tasks.forEach(t => t.stop());
console.log('[CRON] scheduler stopped');
}
module.exports = { startScheduler, stopScheduler };Graceful Shutdown with node-cron
Stop cron tasks before the process exits to avoid orphaned jobs or partial runs during deployments.
// app.js / server.js
const { startScheduler, stopScheduler } = require('./scheduler');
const server = app.listen(3000, () => {
console.log('Server running on port 3000');
startScheduler();
});
function shutdown(signal) {
console.log(`[${signal}] graceful shutdown...`);
stopScheduler();
server.close(() => {
console.log('HTTP server closed');
process.exit(0);
});
// Force exit after 10 s
setTimeout(() => process.exit(1), 10_000).unref();
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));Agenda.js – Persistent Job Scheduling
Agenda stores jobs in MongoDB, supports retries, priority, concurrency limits, and survives process restarts — ideal for email sending, report generation, and delayed jobs.
npm install agenda
// lib/agenda.js
const Agenda = require('agenda');
const agenda = new Agenda({
db: {
address: process.env.MONGO_URI,
collection: 'agendaJobs',
options: { useUnifiedTopology: true },
},
processEvery: '30 seconds',
maxConcurrency: 5,
});
// ── Define jobs ──────────────────────────────────────────────
agenda.define('send welcome email', { priority: 'high', concurrency: 3 },
async (job) => {
const { userId, email } = job.attrs.data;
await mailer.sendWelcome(email);
console.log(`Welcome email sent to ${email}`);
}
);
agenda.define('generate monthly report', async (job) => {
const { month, year } = job.attrs.data;
await reportService.generate(month, year);
});
// ── Start agenda ─────────────────────────────────────────────
async function startAgenda() {
await agenda.start();
// Schedule recurring job
await agenda.every('0 6 1 * *', 'generate monthly report', {
month: new Date().getMonth() + 1,
year: new Date().getFullYear(),
});
console.log('[Agenda] started');
}
// ── Graceful stop ─────────────────────────────────────────────
async function stopAgenda() {
await agenda.stop();
console.log('[Agenda] stopped');
}
module.exports = { agenda, startAgenda, stopAgenda };Scheduling One-Off & Delayed Jobs with Agenda
Use schedule for a specific time or now for immediate execution. Jobs persist in MongoDB — they survive server restarts.
const { agenda } = require('../lib/agenda');
// Fire immediately
await agenda.now('send welcome email', { userId: 42, email: 'user@example.com' });
// Fire in 15 minutes
await agenda.schedule('in 15 minutes', 'send welcome email', {
userId: 43, email: 'another@example.com',
});
// Fire at a specific date/time
await agenda.schedule(new Date('2025-01-01T09:00:00'), 'generate monthly report', {
month: 1, year: 2025,
});
// Cancel a job by name
await agenda.cancel({ name: 'generate monthly report' });
// List all jobs
const jobs = await agenda.jobs({ name: 'send welcome email' });
console.log(jobs.map(j => j.attrs));