Scheduled Tasks
Run tasks on a schedule - cron jobs, recurring tasks, and time-based automation.
6 min read
Scheduling Patterns#
Different ways to run tasks on a schedule:
| Pattern | Use Case | Example |
|---|---|---|
| Cron jobs | Fixed schedule | "Every day at 9 AM" |
| Intervals | Regular polling | "Every 5 minutes" |
| Delayed | One-time future | "In 2 hours" |
| Recurring queue | Reliable scheduled | "Every hour, with retries" |
Cron Syntax#
┌──────────── minute (0-59)
│ ┌────────── hour (0-23)
│ │ ┌──────── day of month (1-31)
│ │ │ ┌────── month (1-12)
│ │ │ │ ┌──── day of week (0-7, 0 and 7 = Sunday)
│ │ │ │ │
* * * * *
Common Patterns#
javascript
'* * * * *' // Every minute
'*/5 * * * *' // Every 5 minutes
'0 * * * *' // Every hour (at minute 0)
'0 0 * * *' // Every day at midnight
'0 9 * * *' // Every day at 9:00 AM
'0 9 * * 1' // Every Monday at 9:00 AM
'0 0 1 * *' // First day of every month
'0 0 * * 0' // Every Sunday at midnight
'0 9-17 * * 1-5' // Weekdays 9 AM - 5 PM, every hour
Option 1: BullMQ Repeatable Jobs#
Best for production - persistent, reliable, distributed:
javascript
// src/queues/scheduledJobs.js
import { Queue, Worker } from 'bullmq';
const connection = { host: 'localhost', port: 6379 };
const scheduledQueue = new Queue('scheduled', { connection });
// Add recurring jobs on startup
export async function setupScheduledJobs() {
// Clear existing repeatables first (optional)
const repeatableJobs = await scheduledQueue.getRepeatableJobs();
for (const job of repeatableJobs) {
await scheduledQueue.removeRepeatableByKey(job.key);
}
// Daily report at 9 AM
await scheduledQueue.add('daily-report', {}, {
repeat: { pattern: '0 9 * * *' },
jobId: 'daily-report',
});
// Cleanup old data every night at 2 AM
await scheduledQueue.add('cleanup', {}, {
repeat: { pattern: '0 2 * * *' },
jobId: 'nightly-cleanup',
});
// Sync external data every 15 minutes
await scheduledQueue.add('external-sync', {}, {
repeat: { every: 15 * 60 * 1000 },
jobId: 'external-sync',
});
// Weekly summary every Monday at 8 AM
await scheduledQueue.add('weekly-summary', {}, {
repeat: { pattern: '0 8 * * 1' },
jobId: 'weekly-summary',
});
console.log('Scheduled jobs configured');
}
// Worker to process scheduled jobs
const worker = new Worker('scheduled', async (job) => {
console.log(`Running scheduled job: ${job.name}`);
switch (job.name) {
case 'daily-report':
await generateDailyReport();
break;
case 'cleanup':
await cleanupOldData();
break;
case 'external-sync':
await syncExternalSources();
break;
case 'weekly-summary':
await sendWeeklySummary();
break;
}
}, { connection });
async function generateDailyReport() {
const stats = await getYesterdayStats();
await sendEmail({
to: 'team@company.com',
subject: `Daily Report - ${new Date().toDateString()}`,
html: generateReportHtml(stats),
});
}
async function cleanupOldData() {
// Delete sessions older than 30 days
await Session.deleteMany({
createdAt: { $lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) }
});
// Delete unverified users older than 7 days
await User.deleteMany({
verified: false,
createdAt: { $lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
});
console.log('Cleanup completed');
}
Option 2: node-cron (Simple)#
Good for single-server, non-critical tasks:
javascript
// src/cron/index.js
import cron from 'node-cron';
import { logger } from '../utils/logger.js';
export function setupCronJobs() {
// Health check every minute
cron.schedule('* * * * *', async () => {
try {
await checkExternalServices();
} catch (error) {
logger.error('Health check failed', error);
}
});
// Daily database backup at 3 AM
cron.schedule('0 3 * * *', async () => {
logger.info('Starting daily backup');
try {
await createDatabaseBackup();
logger.info('Backup completed');
} catch (error) {
logger.error('Backup failed', error);
await notifyAdmin('Backup failed', error);
}
});
// Clear temp files every Sunday at 4 AM
cron.schedule('0 4 * * 0', async () => {
await clearTempFiles();
});
// Expire trials on the 1st of each month
cron.schedule('0 0 1 * *', async () => {
await expireTrialAccounts();
});
logger.info('Cron jobs scheduled');
}
// Start in your main file
import { setupCronJobs } from './cron/index.js';
setupCronJobs();
Option 3: Agenda (MongoDB)#
javascript
import Agenda from 'agenda';
const agenda = new Agenda({
db: { address: process.env.MONGODB_URI },
});
// Define jobs
agenda.define('send reminder emails', async (job) => {
const users = await User.find({ reminderDue: { $lte: new Date() } });
for (const user of users) {
await sendReminderEmail(user);
}
});
agenda.define('generate invoices', async (job) => {
await generateMonthlyInvoices();
});
agenda.define('update exchange rates', async (job) => {
const rates = await fetchExchangeRates();
await ExchangeRate.updateRates(rates);
});
// Schedule
export async function setupAgendaJobs() {
await agenda.start();
// Every day at 10 AM
await agenda.every('0 10 * * *', 'send reminder emails');
// First of every month
await agenda.every('0 0 1 * *', 'generate invoices');
// Every hour
await agenda.every('1 hour', 'update exchange rates');
}
Handling Failures#
With Retries#
javascript
// BullMQ
await queue.add('important-task', data, {
attempts: 5,
backoff: {
type: 'exponential',
delay: 60000, // Start with 1 minute
},
});
// Manual retry in node-cron
cron.schedule('0 * * * *', async () => {
let attempts = 0;
const maxAttempts = 3;
while (attempts < maxAttempts) {
try {
await processData();
break;
} catch (error) {
attempts++;
if (attempts === maxAttempts) {
await alertOps('Scheduled task failed', error);
}
await sleep(5000 * attempts); // Backoff
}
}
});
Dead Letter Queue#
javascript
// Failed jobs go to separate queue
const worker = new Worker('scheduled', processor, {
connection,
settings: {
backoffStrategies: {},
},
});
worker.on('failed', async (job, error) => {
if (job.attemptsMade >= job.opts.attempts) {
// Move to dead letter queue
await deadLetterQueue.add('failed-job', {
originalJob: job.name,
data: job.data,
error: error.message,
failedAt: new Date(),
});
}
});
Timezone Handling#
javascript
// BullMQ with timezone
await queue.add('daily-report', {}, {
repeat: {
pattern: '0 9 * * *',
tz: 'America/New_York', // Run at 9 AM Eastern
},
});
// node-cron with timezone
cron.schedule('0 9 * * *', callback, {
timezone: 'Europe/London'
});
Monitoring Scheduled Jobs#
javascript
// List all scheduled jobs
app.get('/admin/scheduled-jobs', async (req, res) => {
const repeatableJobs = await scheduledQueue.getRepeatableJobs();
const jobs = repeatableJobs.map(job => ({
key: job.key,
name: job.name,
pattern: job.pattern,
every: job.every,
next: job.next,
}));
res.json({ data: jobs });
});
// Get job history
app.get('/admin/scheduled-jobs/:name/history', async (req, res) => {
const completed = await scheduledQueue.getCompleted(0, 10);
const failed = await scheduledQueue.getFailed(0, 10);
const history = [...completed, ...failed]
.filter(job => job.name === req.params.name)
.sort((a, b) => b.finishedOn - a.finishedOn);
res.json({ data: history });
});
Key Takeaways#
- BullMQ for reliability - Persists across restarts
- node-cron for simplicity - Good for single server
- Always handle failures - Retries, alerts, dead letter
- Consider timezones - Users are global
- Monitor your jobs - Know when things fail
Continue Learning
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.