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
$ 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