Cron Jobs and Scheduled Tasks
Not all backend work is triggered by HTTP requests. Some tasks run on a schedule: sending weekly digest emails, cleaning up expired sessions, generating daily reports, archiving old data, or checking the health of external services. In Node.js, scheduled tasks are implemented using cron-style scheduling libraries or BullMQ's repeatable jobs. The two most common libraries are node-cron (simple, in-process scheduling) and node-schedule (more flexible, RRULE support).
node-cron: In-Process Scheduling
node-cron runs scheduled functions within the same Node.js process. It uses a standard cron expression: minute hour day-of-month month day-of-week. The job function executes at the scheduled time. This is simple to set up but has one limitation: if the process restarts mid-job, the job is lost. Use it for lightweight tasks where occasional misses are acceptable.
npm install node-cronconst cron = require('node-cron');
// Cron expression: second(optional) minute hour day month weekday
// '*' = every unit, '0' = at zero, '*/5' = every 5 units
// Runs every day at midnight
cron.schedule('0 0 * * *', async () => {
console.log('[CRON] Running midnight cleanup:', new Date().toISOString());
try {
const result = await Session.deleteMany({ expiresAt: { $lt: new Date() } });
console.log('[CRON] Deleted', result.deletedCount, 'expired sessions');
} catch (err) {
console.error('[CRON] Cleanup failed:', err.message);
}
}, {
timezone: 'Asia/Kolkata' // always specify timezone
});
// Runs every Monday at 9 AM IST
cron.schedule('0 9 * * 1', async () => {
const report = await generateWeeklySalesReport();
await sendReportEmail({ to: '[email protected]', report });
});
// Runs every 5 minutes (health check)
cron.schedule('*/5 * * * *', async () => {
await checkExternalApiHealth();
});BullMQ Repeatable Jobs for Durable Scheduling
For production applications where every scheduled job execution must be guaranteed, use BullMQ repeatable jobs. Unlike node-cron, BullMQ stores the next run time in Redis. If your server restarts or crashes, the job will still run when the server comes back up because Redis remembers when it was due.
const { Queue, Worker } = require('bullmq');
const connection = { host: process.env.REDIS_HOST || 'localhost', port: 6379 };
const scheduledQueue = new Queue('scheduled', { connection });
// Add once; BullMQ manages recurring scheduling in Redis
async function registerScheduledJobs() {
// Remove old definitions to avoid duplicates on restart
await scheduledQueue.removeRepeatableByKey('weekly-report');
await scheduledQueue.removeRepeatableByKey('daily-cleanup');
// Weekly report: every Monday 9 AM IST (UTC+5:30 = 03:30 UTC)
await scheduledQueue.add('weekly-report', {}, {
repeat: { pattern: '30 3 * * 1' }
});
// Daily cleanup: every day at midnight IST (18:30 UTC)
await scheduledQueue.add('daily-cleanup', {}, {
repeat: { pattern: '30 18 * * *' }
});
console.log('[Scheduler] Scheduled jobs registered');
}
// Call this once when your app starts
registerScheduledJobs();How Cron Expressions Work
A cron expression has five (or six) fields: minute (0-59), hour (0-23), day-of-month (1-31), month (1-12), day-of-week (0-7, where 0 and 7 are Sunday). Special characters: * (any), , (list), - (range), / (step). Examples: 0 2 * * * is daily at 2 AM; 0 */6 * * * is every 6 hours; 0 9 1 * * is the 1st of every month at 9 AM; 0 9 * * 1-5 is weekdays at 9 AM.
// Common cron patterns
const SCHEDULES = {
EVERY_MINUTE: '* * * * *',
EVERY_5_MINUTES: '*/5 * * * *',
EVERY_HOUR: '0 * * * *',
DAILY_MIDNIGHT: '0 0 * * *',
DAILY_9AM: '0 9 * * *',
WEEKDAYS_9AM: '0 9 * * 1-5',
MONDAYS_9AM: '0 9 * * 1',
FIRST_OF_MONTH: '0 0 1 * *',
EVERY_SIX_HOURS: '0 */6 * * *'
};
// Validate a cron expression before using it
const cron = require('node-cron');
function isValidCron(expression) {
return cron.validate(expression);
}
console.log(isValidCron('0 9 * * 1')); // true
console.log(isValidCron('99 9 * * 1')); // false (minute 99 invalid)Always specify the timezone option in node-cron. Without it, the schedule runs in the server's system timezone, which may differ between development and production environments — causing jobs to fire at unexpected times.