Skip to main content

Command Palette

Search for a command to run...

Cron Jobs: Schedule Tasks in Node.js

Learn: Cron Jobs: Schedule Tasks in Node.js

Updated
6 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Cron Jobs: Schedule Tasks in Node.js

Problem

Applications often need to execute tasks automatically at specific times or intervals without manual intervention. Common scenarios include:

  • Sending periodic email notifications
  • Cleaning up temporary files or database records
  • Generating reports at scheduled times
  • Syncing data with external APIs
  • Running maintenance tasks during off-peak hours
  • Processing batch jobs overnight

Without a proper scheduling mechanism, developers resort to inefficient workarounds like infinite loops with delays, leading to poor resource management and unreliable execution.

Solution

Node.js provides several approaches to handle scheduled task execution:

  1. Native setInterval() / setTimeout() - Simple but limited, doesn't persist across restarts
  2. Cron expression libraries - Parse and execute tasks based on cron syntax
  3. Job queue systems - Distributed task scheduling with persistence (Bull, RabbitMQ)
  4. Dedicated cron packages - Purpose-built solutions like node-cron or node-schedule

The optimal solution combines a reliable cron library with proper error handling, logging, and graceful shutdown mechanisms.

Code Implementation

1. Basic Setup with node-cron

// npm install node-cron

const cron = require('node-cron');

// Schedule a task to run every minute
cron.schedule('* * * * *', () => {
  console.log('Running a task every minute');
});

// Schedule a task to run at 2:30 AM every day
cron.schedule('30 2 * * *', () => {
  console.log('Running daily maintenance at 2:30 AM');
});

// Cron expression format: minute hour day month day-of-week
// * * * * * (every minute)
// 0 * * * * (every hour)
// 0 0 * * * (daily at midnight)
// 0 0 * * 0 (weekly on Sunday)
// 0 0 1 * * (monthly on the 1st)

2. Production-Ready Implementation

// cronJobs.js
const cron = require('node-cron');
const logger = require('./logger'); // Your logging utility

class CronJobManager {
  constructor() {
    this.jobs = [];
  }

  // Schedule a job with error handling
  scheduleJob(name, cronExpression, taskFunction) {
    try {
      const job = cron.schedule(cronExpression, async () => {
        const startTime = Date.now();
        try {
          logger.info(`[${name}] Job started`);
          await taskFunction();
          const duration = Date.now() - startTime;
          logger.info(`[${name}] Job completed in ${duration}ms`);
        } catch (error) {
          logger.error(`[${name}] Job failed:`, error);
          // Send alert notification
          await this.notifyFailure(name, error);
        }
      });

      this.jobs.push({ name, job });
      logger.info(`[${name}] Cron job scheduled: ${cronExpression}`);
      return job;
    } catch (error) {
      logger.error(`Failed to schedule job ${name}:`, error);
      throw error;
    }
  }

  // Stop a specific job
  stopJob(name) {
    const jobIndex = this.jobs.findIndex(j => j.name === name);
    if (jobIndex !== -1) {
      this.jobs[jobIndex].job.stop();
      this.jobs.splice(jobIndex, 1);
      logger.info(`[${name}] Job stopped`);
    }
  }

  // Stop all jobs
  stopAllJobs() {
    this.jobs.forEach(({ name, job }) => {
      job.stop();
      logger.info(`[${name}] Job stopped`);
    });
    this.jobs = [];
  }

  // Notify on job failure
  async notifyFailure(jobName, error) {
    // Implement your notification logic (email, Slack, etc.)
    logger.error(`Alert: Job ${jobName} failed - ${error.message}`);
  }
}

module.exports = new CronJobManager();

3. Task Definitions

// tasks/emailNotifications.js
const db = require('../database');
const emailService = require('../services/emailService');

async function sendDailyDigest() {
  const users = await db.query('SELECT * FROM users WHERE digest_enabled = true');

  for (const user of users) {
    const digest = await generateDigest(user.id);
    await emailService.send({
      to: user.email,
      subject: 'Your Daily Digest',
      html: digest
    });
  }
}

async function generateDigest(userId) {
  const activities = await db.query(
    'SELECT * FROM activities WHERE user_id = ? AND created_at > NOW() - INTERVAL 1 DAY',
    [userId]
  );
  return `<h1>Your Daily Summary</h1><p>${activities.length} activities</p>`;
}

module.exports = { sendDailyDigest };
// tasks/databaseCleanup.js
const db = require('../database');

async function cleanupExpiredSessions() {
  const result = await db.query(
    'DELETE FROM sessions WHERE expires_at < NOW()'
  );
  console.log(`Cleaned up ${result.affectedRows} expired sessions`);
}

async function archiveOldLogs() {
  const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);

  await db.query(
    'INSERT INTO logs_archive SELECT * FROM logs WHERE created_at < ?',
    [thirtyDaysAgo]
  );

  await db.query(
    'DELETE FROM logs WHERE created_at < ?',
    [thirtyDaysAgo]
  );
}

module.exports = { cleanupExpiredSessions, archiveOldLogs };
// tasks/dataSync.js
const axios = require('axios');
const db = require('../database');

async function syncExternalData() {
  try {
    const response = await axios.get('https://api.example.com/data');
    const data = response.data;

    for (const item of data) {
      await db.query(
        'INSERT INTO external_data (id, content, synced_at) VALUES (?, ?, NOW()) ON DUPLICATE KEY UPDATE content = ?, synced_at = NOW()',
        [item.id, JSON.stringify(item), JSON.stringify(item)]
      );
    }

    console.log(`Synced ${data.length} items`);
  } catch (error) {
    console.error('Data sync failed:', error.message);
    throw error;
  }
}

module.exports = { syncExternalData };

4. Application Initialization

// app.js
const express = require('express');
const cronJobManager = require('./cronJobs');
const { sendDailyDigest } = require('./tasks/emailNotifications');
const { cleanupExpiredSessions, archiveOldLogs } = require('./tasks/databaseCleanup');
const { syncExternalData } = require('./tasks/dataSync');

const app = express();

// Initialize cron jobs
function initializeCronJobs() {
  // Send daily digest at 8 AM
  cronJobManager.scheduleJob(
    'daily-digest',
    '0 8 * * *',
    sendDailyDigest
  );

  // Clean up sessions every hour
  cronJobManager.scheduleJob(
    'cleanup-sessions',
    '0 * * * *',
    cleanupExpiredSessions
  );

  // Archive logs daily at 3 AM
  cronJobManager.scheduleJob(
    'archive-logs',
    '0 3 * * *',
    archiveOldLogs
  );

  // Sync external data every 30 minutes
  cronJobManager.scheduleJob(
    'sync-data',
    '*/30 * * * *',
    syncExternalData
  );
}

// Graceful shutdown
process.on('SIGTERM', () => {
  console.log('SIGTERM received, stopping cron jobs...');
  cronJobManager.stopAllJobs();
  process.exit(0);
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
  initializeCronJobs();
});

module.exports = app;

5. Advanced: Job Validation & Monitoring

// cronJobs.js (enhanced)
const cron = require('node-cron');
const EventEmitter = require('events');

class CronJobManager extends EventEmitter {
  constructor() {
    super();
    this.jobs = [];
    this.metrics = {};
  }

  scheduleJob(name, cronExpression, taskFunction, options = {}) {
    // Validate cron expression
    if (!cron.validate(cronExpression)) {
      throw new Error(`Invalid cron expression: ${cronExpression}`);
    }

    const { maxDuration = 3600000, retries = 0 } = options;
    this.metrics[name] = { runs: 0, failures: 0, lastRun: null };

    const job = cron.schedule(cronExpression, async () => {
      let attempt = 0;

      while (attempt <= retries) {
        try {
          const startTime = Date.now();

          // Execute with timeout
          const result = await Promise.race([
            taskFunction(),
            new Promise((_, reject) =>
              setTimeout(() => reject(new Error('Job timeout')), maxDuration)
            )
          ]);

          const duration = Date.now() - startTime;
          this.metrics[name].runs++;
          this.metrics[name].lastRun = new Date();

          this.emit('job-success', { name, duration, result });
          break;
        } catch (error) {
          attempt++;
          if (attempt > retries) {
            this.metrics[name].failures++;
            this.emit('job-failure', { name, error, attempt });
          }
        }
      }
    });

    this.jobs.push({ name, job });
    return job;
  }

  getMetrics(name) {
    return this.metrics[name] || null;
  }

  getAllMetrics() {
    return this.metrics;
  }
}

module.exports = new CronJobManager();

6. Testing Cron Jobs

// __tests__/cronJobs.test.js
const cronJobManager = require('../cronJobs');

describe('Cron Jobs', () => {
  afterEach(() => {
    cronJobManager.stopAllJobs();
  });

  test('should execute job at scheduled time', (done) => {
    const mockTask = jest.fn();

    cronJobManager.scheduleJob('test-job', '*/1 * * * * *', mockTask);

    setTimeout(() => {
      expect(mockTask).toHaveBeenCalled();
      done();
    }, 1100);
  });

  test('should handle job failures gracefully', (done) => {
    const failingTask = jest.fn().mockRejectedValue(new Error('Task failed'));

    cronJobManager.on('job-failure', ({ name, error }) => {
      expect(name).toBe('failing-job');
      expect(error.message).toBe('Task failed');
      done();
    });

    cronJobManager.scheduleJob('failing-job', '*/1 * * * * *', failingTask);
  });

  test('should validate cron expressions', () => {
    expect(() => {
      cronJobManager.scheduleJob('invalid', 'invalid-expression', () => {});
    }).toThrow('Invalid cron expression');
  });
});

Key Takeaways

  • Use node-cron for simple, reliable cron scheduling
  • Implement error handling with retry logic and notifications
  • Add logging and metrics for monitoring job health
  • Handle graceful shutdown to prevent orphaned processes
  • Validate cron expressions before scheduling
  • Set timeouts to prevent hanging jobs
  • Consider distributed systems (Bull, RabbitMQ) for scalability

This approach ensures reliable, maintainable background job execution in production Node.js applications.