# Cron Jobs: Schedule Tasks in Node.js

# 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

```javascript
// 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

```javascript
// 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

```javascript
// 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 };
```

```javascript
// 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 };
```

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// __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.
