Node.js

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 midnight

node-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));