Scheduled Tasks and Cron Jobs
Scheduled tasks automate recurring operations. Learn to implement cron jobs, task scheduling, and job orchestration in Node.js applications.
Challenge
Section titled “Challenge”Implement reliable scheduled task execution with proper error handling, monitoring, and distributed coordination.
Explanation
Section titled “Explanation”Task Scheduling Patterns
Section titled “Task Scheduling Patterns”- Cron Jobs: Time-based scheduling
- Interval Tasks: Recurring delays
- One-time Tasks: Delayed execution
- Distributed Scheduling: Coordinated multi-node
Popular Libraries
Section titled “Popular Libraries”- node-cron: Simple cron expressions
- node-schedule: Human-readable scheduling
- Bull/BullMQ: Job queue scheduling
- Agenda: Database-backed scheduling
Code Examples
Section titled “Code Examples”1. Node-Cron Basic Scheduling
Section titled “1. Node-Cron Basic Scheduling”const cron = require('node-cron');
// Run every day at 2 AMcron.schedule('0 2 * * *', async () => { console.log('Running daily backup...'); await performBackup();});
// Run every Monday at 9 AMcron.schedule('0 9 * * 1', async () => { console.log('Weekly report generation...'); await generateWeeklyReport();});
// Run every 5 minutescron.schedule('*/5 * * * *', async () => { console.log('Checking for pending tasks...'); await processPendingTasks();});
// Cron pattern: minute hour day-of-month month day-of-week// Field Allowed values// Minute 0-59// Hour 0-23// Day of month 1-31// Month 1-12 (or names)// Day of week 0-7 (0 and 7 are Sunday, or names)
// Examples:// 0 0 1 * * - 1st day of month at midnight// 30 23 * * 5 - Friday at 11:30 PM// 0 * * * * - Every hour// 0 0 * * * - Every day at midnight// 0 10-12 * * * - 10 AM, 11 AM, 12 PM every day2. Task Monitoring and Logging
Section titled “2. Task Monitoring and Logging”const cron = require('node-cron');const logger = require('./logger');
class ScheduledTask { constructor(name, cronExpression, handler) { this.name = name; this.cronExpression = cronExpression; this.handler = handler; this.lastRun = null; this.lastResult = null; this.failureCount = 0; this.maxRetries = 3; }
start() { this.task = cron.schedule(this.cronExpression, async () => { try { logger.info(`Starting task: ${this.name}`); const startTime = Date.now();
this.lastResult = await this.handler(); this.lastRun = new Date(); this.failureCount = 0;
const duration = Date.now() - startTime; logger.info(`Task completed: ${this.name} (${duration}ms)`); } catch (error) { this.failureCount++; logger.error(`Task failed: ${this.name}`, { error: error.message, attempt: this.failureCount, });
if (this.failureCount >= this.maxRetries) { logger.error(`Task exceeded max retries: ${this.name}`); // Alert monitoring system } } }); }
stop() { this.task.stop(); }
getStatus() { return { name: this.name, lastRun: this.lastRun, lastResult: this.lastResult, failureCount: this.failureCount, running: this.task?.status === 'started', }; }}
// Usageconst backupTask = new ScheduledTask( 'Database Backup', '0 2 * * *', async () => { return await performBackup(); });
backupTask.start();
// Endpoint to check task statusapp.get('/admin/scheduled-tasks', (req, res) => { res.json({ backup: backupTask.getStatus(), });});3. Distributed Task Scheduling with Agenda
Section titled “3. Distributed Task Scheduling with Agenda”const Agenda = require('agenda');
const agenda = new Agenda({ db: { address: 'mongodb://localhost:27017/scheduler', },});
// Define jobsagenda.define('send-email-notification', async (job) => { const { userId, template } = job.attrs.data; console.log(`Sending ${template} email to user ${userId}`);
await emailService.send(userId, template);});
agenda.define('cleanup-expired-sessions', async () => { console.log('Cleaning up expired sessions');
const expiredCount = await database.sessions.deleteExpired(); console.log(`Deleted ${expiredCount} expired sessions`);});
agenda.define('generate-report', async (job) => { const { reportType, userId } = job.attrs.data; console.log(`Generating ${reportType} report for user ${reportType}`);
const report = await generateReport(reportType); await notificationService.notify(userId, 'Report ready', report);});
// Start agenda(async () => { await agenda.start();
// Schedule recurring jobs agenda.every('0 2 * * *', 'cleanup-expired-sessions');
// Schedule one-time job agenda.schedule('in 1 hour', 'send-email-notification', { userId: 'user-123', template: 'welcome', });
// Schedule repeating job with limit agenda.every('*/5 * * * *', 'check-pending-tasks', {}, { skipImmediate: true });})();
// API to schedule on-demandapp.post('/admin/schedule-report', async (req, res) => { const job = await agenda.schedule('tomorrow at 9am', 'generate-report', { reportType: req.body.type, userId: req.user.id, });
res.json({ jobId: job.attrs._id });});4. Task Queue with Bull and Recurring Jobs
Section titled “4. Task Queue with Bull and Recurring Jobs”const Queue = require('bull');
const taskQueue = new Queue('scheduled-tasks', { redis: { host: 'localhost', port: 6379 },});
class ScheduledTaskManager { constructor(queue) { this.queue = queue; this.tasks = new Map(); }
async registerRecurringTask(name, cronExpression, handler, options = {}) { const job = await this.queue.add( { task: name, handler }, { repeat: { cron: cronExpression, tz: 'America/New_York', }, jobId: `recurring-${name}`, ...options, } );
this.tasks.set(name, job); console.log(`Registered recurring task: ${name} (${cronExpression})`); }
processTask(name, handler) { this.queue.process(name, async (job) => { try { const startTime = Date.now(); const result = await handler(job.data);
console.log(`Task ${name} completed in ${Date.now() - startTime}ms`); return result; } catch (error) { console.error(`Task ${name} failed:`, error.message); throw error; } }); }
async cancelTask(name) { const job = this.tasks.get(name); if (job) { await job.remove(); this.tasks.delete(name); console.log(`Cancelled task: ${name}`); } }}
// Usageconst taskManager = new ScheduledTaskManager(taskQueue);
// Register taskstaskManager.registerRecurringTask( 'daily-backup', '0 2 * * *', async (data) => { await performBackup(); return { status: 'completed' }; });
// Process taskstaskManager.processTask('daily-backup', async (data) => { return await performBackup();});5. Rate-Limited Task Execution
Section titled “5. Rate-Limited Task Execution”class RateLimitedScheduler { constructor(tasksPerHour = 100) { this.tasksPerHour = tasksPerHour; this.taskTimes = []; }
canExecuteTask() { const now = Date.now(); const oneHourAgo = now - 60 * 60 * 1000;
// Remove old task times this.taskTimes = this.taskTimes.filter(time => time > oneHourAgo);
return this.taskTimes.length < this.tasksPerHour; }
async executeTask(handler) { if (!this.canExecuteTask()) { const nextSlot = Math.min(...this.taskTimes) + 60 * 60 * 1000; const waitTime = nextSlot - Date.now();
console.log(`Rate limit reached. Next execution in ${Math.round(waitTime / 1000)}s`); await this.wait(waitTime); }
this.taskTimes.push(Date.now()); return await handler(); }
wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }}
// Usageconst scheduler = new RateLimitedScheduler(100); // 100 tasks per hour
cron.schedule('* * * * *', async () => { await scheduler.executeTask(async () => { await processTask(); });});6. Health Check and Alerting
Section titled “6. Health Check and Alerting”class ScheduledTaskMonitor { constructor() { this.tasks = new Map(); this.alerts = []; }
registerTask(name, expectedInterval) { this.tasks.set(name, { name, expectedInterval, lastRun: null, nextExpectedRun: Date.now() + expectedInterval, failures: 0, }); }
recordTaskRun(name) { const task = this.tasks.get(name); if (!task) return;
const now = Date.now(); const delay = now - task.nextExpectedRun;
if (delay > 60000) { // More than 1 minute late this.alert(`Task ${name} is running late (${Math.round(delay / 1000)}s)`); }
task.lastRun = now; task.nextExpectedRun = now + task.expectedInterval; task.failures = 0; }
recordTaskFailure(name) { const task = this.tasks.get(name); if (!task) return;
task.failures++;
if (task.failures >= 3) { this.alert(`Task ${name} has failed ${task.failures} times`); } }
getHealthStatus() { const now = Date.now(); const tasks = [];
for (const [name, task] of this.tasks) { const isLate = now > task.nextExpectedRun; tasks.push({ name, lastRun: task.lastRun, isLate, failures: task.failures, status: task.failures > 0 ? 'warning' : (isLate ? 'late' : 'healthy'), }); }
return { tasks, alerts: this.alerts }; }
alert(message) { console.warn('ALERT:', message); this.alerts.push({ timestamp: new Date(), message, });
// Send to monitoring service monitoringService.reportAlert({ service: 'scheduler', message, severity: 'warning', }); }}
// Endpoint for health checkconst monitor = new ScheduledTaskMonitor();monitor.registerTask('daily-backup', 24 * 60 * 60 * 1000);
app.get('/health/scheduler', (req, res) => { const status = monitor.getHealthStatus(); res.json(status);});Best Practices
Section titled “Best Practices”-
Reliability
- Set up monitoring and alerts
- Implement retry logic
- Use persistent job storage
- Track execution history
-
Timing
- Account for execution duration
- Avoid overlapping executions
- Use appropriate time zones
- Test scheduling thoroughly
-
Distribution
- Prevent duplicate execution on multiple nodes
- Use database or Redis for coordination
- Implement leader election
- Monitor cluster health
-
Performance
- Don’t block the event loop
- Use background workers
- Implement rate limiting
- Scale horizontally
-
Maintenance
- Document all scheduled tasks
- Version control task definitions
- Provide easy enable/disable
- Log all executions
Real-World Applications
Section titled “Real-World Applications”- Data Backups: Automated database backups
- Reports: Scheduled report generation
- Cleanup: Expired session/cache cleanup
- Monitoring: Periodic health checks
- Notifications: Batch email/SMS delivery