Tools: Cron Jobs in Node.js: The Practical Guide Nobody Gave Me (2026)

Tools: Cron Jobs in Node.js: The Practical Guide Nobody Gave Me (2026)

Cron Jobs in Node.js: The Practical Guide Nobody Gave Me

The Basics Everyone Shows You

1. Your Process Will Crash (And Your Cron Jobs Die)

Solution: Wrap Everything

2. Timezones Will Bite You

Solution: Be Explicit

3. Overlapping Runs Are a Real Problem

Solution: File-Based Lock

4. Persistence: Surviving Restarts

What Needs Persisting

5. Logging: Make It Searchable

6. The Production Setup That Actually Works

Quick Reference: Cron Expression Cheat Sheet

What About Alternatives? I spent 3 days learning things about cron in Node.js that should have been documented somewhere. Here is everything I wish I had known. Simple enough. node-cron has 50k+ weekly downloads for a reason. But here is what the tutorials do NOT tell you. This is the #1 thing that bit me: When an unhandled exception occurs inside your cron callback, it kills your whole Node.js process, including all other cron jobs. Even better — make a wrapper: It runs at 9 AM server local time. If your server is in UTC and you are in Hong Kong (UTC+8), that is 5 PM your time, not 9 AM. What happens when a job takes longer than its interval? Result: Multiple instances running simultaneously. Race conditions. Database locks. Chaos. Your VPS reboots. Power goes out. You deploy new code. All your in-memory cron state is gone. Do not just console.log. You will regret it when you need to debug something from 2 weeks ago: Here is my complete setup for a 24/7 Node.js process with cron: With systemd service file: For most side projects: node-cron + systemd. Simple, reliable, zero extra infrastructure. What cron pitfalls have you encountered? Drop a comment below. More from Alex Chen: My Blog | Docker vs systemd Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ const cron = require('node-cron'); // Run every 5 minutes cron.schedule('*/5 * * * *', () => { console.log('Running every 5 minutes'); }); // Run daily at 9 AM cron.schedule('0 9 * * *', () => { console.log('Good morning!'); }); const cron = require('node-cron'); // Run every 5 minutes cron.schedule('*/5 * * * *', () => { console.log('Running every 5 minutes'); }); // Run daily at 9 AM cron.schedule('0 9 * * *', () => { console.log('Good morning!'); }); const cron = require('node-cron'); // Run every 5 minutes cron.schedule('*/5 * * * *', () => { console.log('Running every 5 minutes'); }); // Run daily at 9 AM cron.schedule('0 9 * * *', () => { console.log('Good morning!'); }); // app.js const cron = require('node-cron'); cron.schedule('*/5 * * * *', async () => { // If THIS throws, the entire process dies const data = await fetchSomethingThatMightFail(); processData(data); // Can also throw }); // app.js const cron = require('node-cron'); cron.schedule('*/5 * * * *', async () => { // If THIS throws, the entire process dies const data = await fetchSomethingThatMightFail(); processData(data); // Can also throw }); // app.js const cron = require('node-cron'); cron.schedule('*/5 * * * *', async () => { // If THIS throws, the entire process dies const data = await fetchSomethingThatMightFail(); processData(data); // Can also throw }); cron.schedule('*/5 * * * *', async () => { try { const data = await await fetchSomethingThatMightFail(); processData(data); } catch (err) { console.error('[cron] Task failed:', err.message); // Process keeps running! Other jobs unaffected! } }); cron.schedule('*/5 * * * *', async () => { try { const data = await await fetchSomethingThatMightFail(); processData(data); } catch (err) { console.error('[cron] Task failed:', err.message); // Process keeps running! Other jobs unaffected! } }); cron.schedule('*/5 * * * *', async () => { try { const data = await await fetchSomethingThatMightFail(); processData(data); } catch (err) { console.error('[cron] Task failed:', err.message); // Process keeps running! Other jobs unaffected! } }); function safeJob(name, fn) { return async () => { const -weight: 500;">start = Date.now(); try { await fn(); console.log(`[cron] ${name} OK (${Date.now()--weight: 500;">start}ms)`); } catch (err) { console.error(`[cron] ${name} FAILED: ${err.message}`); // Optionally send alert } }; } // Usage cron.schedule('*/5 * * * *', safeJob('check-prs', async () => { const prs = await checkGitHubPRs(); if (prs.length > 0) await notifyTeam(prs)); })); function safeJob(name, fn) { return async () => { const -weight: 500;">start = Date.now(); try { await fn(); console.log(`[cron] ${name} OK (${Date.now()--weight: 500;">start}ms)`); } catch (err) { console.error(`[cron] ${name} FAILED: ${err.message}`); // Optionally send alert } }; } // Usage cron.schedule('*/5 * * * *', safeJob('check-prs', async () => { const prs = await checkGitHubPRs(); if (prs.length > 0) await notifyTeam(prs)); })); function safeJob(name, fn) { return async () => { const -weight: 500;">start = Date.now(); try { await fn(); console.log(`[cron] ${name} OK (${Date.now()--weight: 500;">start}ms)`); } catch (err) { console.error(`[cron] ${name} FAILED: ${err.message}`); // Optionally send alert } }; } // Usage cron.schedule('*/5 * * * *', safeJob('check-prs', async () => { const prs = await checkGitHubPRs(); if (prs.length > 0) await notifyTeam(prs)); })); // This runs at 9 AM... but whose 9 AM? cron.schedule('0 9 * * *', () => { ... }); // This runs at 9 AM... but whose 9 AM? cron.schedule('0 9 * * *', () => { ... }); // This runs at 9 AM... but whose 9 AM? cron.schedule('0 9 * * *', () => { ... }); const cron = require('node-cron'); // Method 1: Specify timezone cron.schedule('0 9 * * *', () => { // Runs at 9 AM Hong Kong time }, { timezone: 'Asia/Hong_Kong' }); // Method 2: Use UTC and calculate offset // 9 AM Hong Kong = 1 AM UTC cron.schedule('0 1 * * *', () => { // Runs at 9 AM HKT (assuming server is UTC) }); // Method 3: Use luxon for clarity const { DateTime } = require('luxon'); const hktNow = DateTime.now().setZone('Asia/Hong_Kong'); console.log(hktNow.hour); // Current hour in HKT const cron = require('node-cron'); // Method 1: Specify timezone cron.schedule('0 9 * * *', () => { // Runs at 9 AM Hong Kong time }, { timezone: 'Asia/Hong_Kong' }); // Method 2: Use UTC and calculate offset // 9 AM Hong Kong = 1 AM UTC cron.schedule('0 1 * * *', () => { // Runs at 9 AM HKT (assuming server is UTC) }); // Method 3: Use luxon for clarity const { DateTime } = require('luxon'); const hktNow = DateTime.now().setZone('Asia/Hong_Kong'); console.log(hktNow.hour); // Current hour in HKT const cron = require('node-cron'); // Method 1: Specify timezone cron.schedule('0 9 * * *', () => { // Runs at 9 AM Hong Kong time }, { timezone: 'Asia/Hong_Kong' }); // Method 2: Use UTC and calculate offset // 9 AM Hong Kong = 1 AM UTC cron.schedule('0 1 * * *', () => { // Runs at 9 AM HKT (assuming server is UTC) }); // Method 3: Use luxon for clarity const { DateTime } = require('luxon'); const hktNow = DateTime.now().setZone('Asia/Hong_Kong'); console.log(hktNow.hour); // Current hour in HKT // Runs every 5 minutes cron.schedule('*/5 * * * *', async () => { // But this task takes 8 minutes! await longRunningTask(); }); // Runs every 5 minutes cron.schedule('*/5 * * * *', async () => { // But this task takes 8 minutes! await longRunningTask(); }); // Runs every 5 minutes cron.schedule('*/5 * * * *', async () => { // But this task takes 8 minutes! await longRunningTask(); }); const fs = require('fs'); const path = require('path'); function preventOverlap(jobName, fn) { const lockFile = path.join(__dirname, '.cron-locks', jobName + '.lock'); return async () => { // Check if lock exists if (fs.existsSync(lockFile)) { const age = Date.now() - fs.statSync(lockFile).mtimeMs; if (age < 30 * 60 * 1000) { // Lock younger than 30 min console.log(`[cron] ${jobName}: Skipping (already running)`); return; // Skip this run } // Lock is stale (>30 min), -weight: 500;">remove it fs.unlinkSync(lockFile); } // Create lock fs.mkdirSync(path.dirname(lockFile), { recursive: true }); fs.writeFileSync(lockFile, process.pid.toString()); try { await fn(); } finally { // Always -weight: 500;">remove lock if (fs.existsSync(lockFile)) fs.unlinkSync(lockFile); } }; } // Usage cron.schedule('*/5 * * * *', preventOverlap('long-task', async () => { await longRunningTask(); // Safe now! })); const fs = require('fs'); const path = require('path'); function preventOverlap(jobName, fn) { const lockFile = path.join(__dirname, '.cron-locks', jobName + '.lock'); return async () => { // Check if lock exists if (fs.existsSync(lockFile)) { const age = Date.now() - fs.statSync(lockFile).mtimeMs; if (age < 30 * 60 * 1000) { // Lock younger than 30 min console.log(`[cron] ${jobName}: Skipping (already running)`); return; // Skip this run } // Lock is stale (>30 min), -weight: 500;">remove it fs.unlinkSync(lockFile); } // Create lock fs.mkdirSync(path.dirname(lockFile), { recursive: true }); fs.writeFileSync(lockFile, process.pid.toString()); try { await fn(); } finally { // Always -weight: 500;">remove lock if (fs.existsSync(lockFile)) fs.unlinkSync(lockFile); } }; } // Usage cron.schedule('*/5 * * * *', preventOverlap('long-task', async () => { await longRunningTask(); // Safe now! })); const fs = require('fs'); const path = require('path'); function preventOverlap(jobName, fn) { const lockFile = path.join(__dirname, '.cron-locks', jobName + '.lock'); return async () => { // Check if lock exists if (fs.existsSync(lockFile)) { const age = Date.now() - fs.statSync(lockFile).mtimeMs; if (age < 30 * 60 * 1000) { // Lock younger than 30 min console.log(`[cron] ${jobName}: Skipping (already running)`); return; // Skip this run } // Lock is stale (>30 min), -weight: 500;">remove it fs.unlinkSync(lockFile); } // Create lock fs.mkdirSync(path.dirname(lockFile), { recursive: true }); fs.writeFileSync(lockFile, process.pid.toString()); try { await fn(); } finally { // Always -weight: 500;">remove lock if (fs.existsSync(lockFile)) fs.unlinkSync(lockFile); } }; } // Usage cron.schedule('*/5 * * * *', preventOverlap('long-task', async () => { await longRunningTask(); // Safe now! })); class CronState { constructor(stateDir) { this.stateDir = stateDir; fs.mkdirSync(stateDir, { recursive: true }); } getLastRun(jobName) { const file = path.join(this.stateDir, jobName + '.json'); try { return JSON.parse(fs.readFileSync(file, 'utf8')).lastRun; } catch { return null; } } setLastRun(jobName) { const file = path.join(this.stateDir, jobName + '.json'); fs.writeFileSync(file, JSON.stringify({ lastRun: Date.now(), pid: process.pid })); } shouldRun(jobName, intervalMs) { const lastRun = this.getLastRun(jobName); if (!lastRun) return true; return (Date.now() - lastRun) >= intervalMs; } } const state = new CronState('./cron-state'); cron.schedule('*/5 * * * *', () => { if (!state.shouldRun('check-alerts', 5 * 60 * 1000)) return; checkAlerts(); state.setLastRun('check-alerts'); }); class CronState { constructor(stateDir) { this.stateDir = stateDir; fs.mkdirSync(stateDir, { recursive: true }); } getLastRun(jobName) { const file = path.join(this.stateDir, jobName + '.json'); try { return JSON.parse(fs.readFileSync(file, 'utf8')).lastRun; } catch { return null; } } setLastRun(jobName) { const file = path.join(this.stateDir, jobName + '.json'); fs.writeFileSync(file, JSON.stringify({ lastRun: Date.now(), pid: process.pid })); } shouldRun(jobName, intervalMs) { const lastRun = this.getLastRun(jobName); if (!lastRun) return true; return (Date.now() - lastRun) >= intervalMs; } } const state = new CronState('./cron-state'); cron.schedule('*/5 * * * *', () => { if (!state.shouldRun('check-alerts', 5 * 60 * 1000)) return; checkAlerts(); state.setLastRun('check-alerts'); }); class CronState { constructor(stateDir) { this.stateDir = stateDir; fs.mkdirSync(stateDir, { recursive: true }); } getLastRun(jobName) { const file = path.join(this.stateDir, jobName + '.json'); try { return JSON.parse(fs.readFileSync(file, 'utf8')).lastRun; } catch { return null; } } setLastRun(jobName) { const file = path.join(this.stateDir, jobName + '.json'); fs.writeFileSync(file, JSON.stringify({ lastRun: Date.now(), pid: process.pid })); } shouldRun(jobName, intervalMs) { const lastRun = this.getLastRun(jobName); if (!lastRun) return true; return (Date.now() - lastRun) >= intervalMs; } } const state = new CronState('./cron-state'); cron.schedule('*/5 * * * *', () => { if (!state.shouldRun('check-alerts', 5 * 60 * 1000)) return; checkAlerts(); state.setLastRun('check-alerts'); }); const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'cron-error.log', level: 'error' }), new winston.transports.File({ filename: 'cron-combined.log' }), new winston.transports.Console() // Also see in terminal ] }); // In your cron job logger.info('PR check completed', { count: 25, duration_ms: 1234 }); logger.error('Failed to fetch data', { error: err.message, url: targetUrl }); // Search later: // grep -i "error" cron-combined.log | tail -20 // cat cron-combined.log | jq 'select(.level=="error")' const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'cron-error.log', level: 'error' }), new winston.transports.File({ filename: 'cron-combined.log' }), new winston.transports.Console() // Also see in terminal ] }); // In your cron job logger.info('PR check completed', { count: 25, duration_ms: 1234 }); logger.error('Failed to fetch data', { error: err.message, url: targetUrl }); // Search later: // grep -i "error" cron-combined.log | tail -20 // cat cron-combined.log | jq 'select(.level=="error")' const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'cron-error.log', level: 'error' }), new winston.transports.File({ filename: 'cron-combined.log' }), new winston.transports.Console() // Also see in terminal ] }); // In your cron job logger.info('PR check completed', { count: 25, duration_ms: 1234 }); logger.error('Failed to fetch data', { error: err.message, url: targetUrl }); // Search later: // grep -i "error" cron-combined.log | tail -20 // cat cron-combined.log | jq 'select(.level=="error")' // index.js — entry point const cron = require('node-cron'); const logger = require('./logger'); const state = new (require('./cron-state'))('./cron-state'); // --- Utility wrappers --- function job(name, interval, fn) { return cron.schedule(interval, async () => { if (!state.shouldRun(name, parseInterval(interval))) return; const -weight: 500;">start = Date.now(); try { await fn(); logger.info(`${name} completed`, { duration_ms: Date.now() - -weight: 500;">start }); } catch (err) { logger.error(`${name} failed`, { error: err.message, stack: err.stack }); } finally { state.setLastRun(name); } }); } // --- Scheduled tasks --- job('health-check', '*/5 * * * *', async () => { await checkAllEndpoints(); }); job('pr-monitor', '*/30 * * * *', async () => { const changes = await monitorGitHubPRs(); if (changes.length > 0) await sendAlert(changes); }); job('daily-report', '0 9 * * *', async () => { const report = await generateDailyReport(); await emailReport(report); }, { timezone: 'Asia/Hong_Kong' }); // 9 AM HKT job('cleanup', '0 3 * * 0', async () => { // Weekly cleanup on Sundays at 3 AM await cleanOldLogs(); await pruneTempFiles(); }); // --- Graceful shutdown --- process.on('SIGTERM', () => { logger.info('Shutting down gracefully...'); cron.getTasks().forEach(task => task.-weight: 500;">stop()); setTimeout(() => process.exit(0), 5000); }); logger.info('Cron scheduler started', { pid: process.pid }); // index.js — entry point const cron = require('node-cron'); const logger = require('./logger'); const state = new (require('./cron-state'))('./cron-state'); // --- Utility wrappers --- function job(name, interval, fn) { return cron.schedule(interval, async () => { if (!state.shouldRun(name, parseInterval(interval))) return; const -weight: 500;">start = Date.now(); try { await fn(); logger.info(`${name} completed`, { duration_ms: Date.now() - -weight: 500;">start }); } catch (err) { logger.error(`${name} failed`, { error: err.message, stack: err.stack }); } finally { state.setLastRun(name); } }); } // --- Scheduled tasks --- job('health-check', '*/5 * * * *', async () => { await checkAllEndpoints(); }); job('pr-monitor', '*/30 * * * *', async () => { const changes = await monitorGitHubPRs(); if (changes.length > 0) await sendAlert(changes); }); job('daily-report', '0 9 * * *', async () => { const report = await generateDailyReport(); await emailReport(report); }, { timezone: 'Asia/Hong_Kong' }); // 9 AM HKT job('cleanup', '0 3 * * 0', async () => { // Weekly cleanup on Sundays at 3 AM await cleanOldLogs(); await pruneTempFiles(); }); // --- Graceful shutdown --- process.on('SIGTERM', () => { logger.info('Shutting down gracefully...'); cron.getTasks().forEach(task => task.-weight: 500;">stop()); setTimeout(() => process.exit(0), 5000); }); logger.info('Cron scheduler started', { pid: process.pid }); // index.js — entry point const cron = require('node-cron'); const logger = require('./logger'); const state = new (require('./cron-state'))('./cron-state'); // --- Utility wrappers --- function job(name, interval, fn) { return cron.schedule(interval, async () => { if (!state.shouldRun(name, parseInterval(interval))) return; const -weight: 500;">start = Date.now(); try { await fn(); logger.info(`${name} completed`, { duration_ms: Date.now() - -weight: 500;">start }); } catch (err) { logger.error(`${name} failed`, { error: err.message, stack: err.stack }); } finally { state.setLastRun(name); } }); } // --- Scheduled tasks --- job('health-check', '*/5 * * * *', async () => { await checkAllEndpoints(); }); job('pr-monitor', '*/30 * * * *', async () => { const changes = await monitorGitHubPRs(); if (changes.length > 0) await sendAlert(changes); }); job('daily-report', '0 9 * * *', async () => { const report = await generateDailyReport(); await emailReport(report); }, { timezone: 'Asia/Hong_Kong' }); // 9 AM HKT job('cleanup', '0 3 * * 0', async () => { // Weekly cleanup on Sundays at 3 AM await cleanOldLogs(); await pruneTempFiles(); }); // --- Graceful shutdown --- process.on('SIGTERM', () => { logger.info('Shutting down gracefully...'); cron.getTasks().forEach(task => task.-weight: 500;">stop()); setTimeout(() => process.exit(0), 5000); }); logger.info('Cron scheduler started', { pid: process.pid }); [Unit] Description=Cron Worker After=network.target [Service] Type=simple ExecStart=/usr/bin/node /opt/app/index.js WorkingDirectory=/opt/app Restart=always RestartSec=10 Environment=NODE_ENV=production [Install] WantedBy=multi-user.target [Unit] Description=Cron Worker After=network.target [Service] Type=simple ExecStart=/usr/bin/node /opt/app/index.js WorkingDirectory=/opt/app Restart=always RestartSec=10 Environment=NODE_ENV=production [Install] WantedBy=multi-user.target [Unit] Description=Cron Worker After=network.target [Service] Type=simple ExecStart=/usr/bin/node /opt/app/index.js WorkingDirectory=/opt/app Restart=always RestartSec=10 Environment=NODE_ENV=production [Install] WantedBy=multi-user.target