Task Scheduling: Distributed Cron Alternative
Learn: Task Scheduling: Distributed Cron Alternative
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
Task Scheduling: Distributed Cron Alternative with Agenda & node-schedule
Problem
Traditional cron jobs have significant limitations in modern distributed systems:
- Single-machine dependency: Cron runs only on one server; if it crashes, scheduled tasks fail
- No persistence: Job state is lost on restart
- Poor scalability: Difficult to manage across multiple instances
- Limited monitoring: No built-in visibility into job execution
- No retry logic: Failed tasks aren't automatically retried
- Timezone complexity: Managing timezones across systems is cumbersome
- No job queuing: Tasks execute immediately or not at all
Solution
Two complementary Node.js libraries solve these problems:
- node-schedule: Simple, in-process scheduling for single-server scenarios
- Agenda: Distributed job queue with MongoDB persistence for production systems
Both provide:
- Flexible scheduling syntax
- Job persistence and recovery
- Retry mechanisms
- Timezone support
- Event-driven architecture
- Graceful shutdown handling
Code Implementation
1. node-schedule: Simple Local Scheduling
// simple-scheduler.js
const schedule = require('node-schedule');
// Basic cron-like syntax
const job1 = schedule.scheduleJob('0 9 * * 1-5', () => {
console.log('Weekday 9 AM task executed');
sendDailyReport();
});
// Recurrence rule for complex patterns
const rule = new schedule.RecurrenceRule();
rule.minute = [0, 15, 30, 45];
rule.hour = 9;
rule.dayOfWeek = [0, 6]; // Weekend
const job2 = schedule.scheduleJob(rule, () => {
console.log('Weekend 9 AM every 15 minutes');
checkSystemHealth();
});
// Date-based scheduling
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(14, 30, 0, 0);
const job3 = schedule.scheduleJob(tomorrow, () => {
console.log('One-time task tomorrow at 2:30 PM');
generateMonthlyReport();
});
// Interval-based scheduling
const job4 = schedule.scheduleJob('*/5 * * * *', () => {
console.log('Every 5 minutes');
syncData();
});
// Cancel a job
job1.cancel();
// Graceful shutdown
process.on('SIGTERM', () => {
schedule.gracefulShutdown();
process.exit(0);
});
2. Agenda: Production-Grade Distributed Scheduling
// agenda-setup.js
const Agenda = require('agenda');
const mongoConnectionString = 'mongodb://localhost:27017/jobs';
const agenda = new Agenda({ db: { address: mongoConnectionString } });
// Define job types
agenda.define('send-email', async (job) => {
const { email, subject } = job.attrs.data;
console.log(`Sending email to ${email}: ${subject}`);
try {
await sendEmail(email, subject);
console.log('Email sent successfully');
} catch (error) {
console.error('Email failed:', error);
throw error; // Agenda will retry
}
});
agenda.define('cleanup-old-files', async (job) => {
console.log('Starting cleanup job');
const deletedCount = await deleteFilesOlderThan(30);
job.attrs.data.deletedCount = deletedCount;
console.log(`Deleted ${deletedCount} files`);
});
agenda.define('generate-report', async (job) => {
const { reportType, userId } = job.attrs.data;
console.log(`Generating ${reportType} report for user ${userId}`);
const report = await generateReport(reportType, userId);
await saveReport(report);
});
// Configure retry logic
agenda.define('process-payment',
{
concurrency: 5,
lockLimit: 0,
lockInterval: 5000,
},
async (job) => {
const { paymentId } = job.attrs.data;
console.log(`Processing payment ${paymentId}`);
try {
await processPayment(paymentId);
} catch (error) {
// Automatic retry with exponential backoff
throw error;
}
}
);
// Start agenda
(async () => {
await agenda.start();
console.log('Agenda started');
})();
module.exports = agenda;
3. Scheduling Jobs with Agenda
// job-scheduler.js
const agenda = require('./agenda-setup');
// Schedule recurring jobs
async function scheduleRecurringJobs() {
// Every day at 2 AM
agenda.every('0 2 * * *', 'cleanup-old-files', {
retries: 3,
priority: 'high',
});
// Every Monday at 9 AM
agenda.every('0 9 * * 1', 'generate-report', {
reportType: 'weekly',
userId: 'admin',
});
// Every 30 minutes
agenda.every('*/30 * * * *', 'send-email', {
email: 'alerts@company.com',
subject: 'System Status Update',
});
// One-time job in 5 minutes
const job = agenda.schedule(new Date(Date.now() + 5 * 60000), 'process-payment', {
paymentId: 'PAY-12345',
});
console.log('Jobs scheduled');
}
// Handle job events
agenda.on('success', (job) => {
console.log(`✓ Job succeeded: ${job.attrs.name}`);
});
agenda.on('fail', (error, job) => {
console.error(`✗ Job failed: ${job.attrs.name}`, error.message);
});
agenda.on('start', (job) => {
console.log(`→ Job started: ${job.attrs.name}`);
});
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('Shutting down gracefully...');
await agenda.stop();
process.exit(0);
});
scheduleRecurringJobs().catch(console.error);
4. Advanced: Multi-Instance Coordination
// distributed-scheduler.js
const Agenda = require('agenda');
const os = require('os');
const agenda = new Agenda({
db: { address: 'mongodb://localhost:27017/jobs' },
processEvery: '30 seconds',
maxConcurrency: 20,
defaultConcurrency: 5,
lockLimit: 0,
});
const instanceId = `${os.hostname()}-${process.pid}`;
agenda.define('heavy-computation',
{ concurrency: 1, lockLimit: 1 },
async (job) => {
console.log(`[${instanceId}] Running heavy computation`);
// Only one instance runs this at a time
const result = await performHeavyComputation();
job.attrs.data.result = result;
}
);
agenda.define('distributed-task',
{ concurrency: 3 },
async (job) => {
console.log(`[${instanceId}] Processing distributed task`);
// Multiple instances can run this concurrently
const { taskId } = job.attrs.data;
await processTask(taskId);
}
);
// Health check job
agenda.define('health-check', async (job) => {
const health = {
instance: instanceId,
timestamp: new Date(),
uptime: process.uptime(),
memory: process.memoryUsage(),
};
console.log('Health check:', health);
await saveHealthMetrics(health);
});
// Schedule health check every minute
agenda.every('* * * * *', 'health-check');
// Start with error handling
(async () => {
try {
await agenda.start();
console.log(`Agenda started on ${instanceId}`);
// Schedule jobs
agenda.every('0 3 * * *', 'heavy-computation');
agenda.every('*/10 * * * *', 'distributed-task', { taskId: 'batch-1' });
} catch (error) {
console.error('Failed to start agenda:', error);
process.exit(1);
}
})();
module.exports = agenda;
5. Express Integration with Job Management UI
// server.js
const express = require('express');
const agenda = require('./distributed-scheduler');
const app = express();
app.use(express.json());
// Get all jobs
app.get('/api/jobs', async (req, res) => {
try {
const jobs = await agenda.jobs({});
res.json(jobs.map(job => ({
id: job._id,
name: job.attrs.name,
nextRunAt: job.attrs.nextRunAt,
lastRunAt: job.attrs.lastRunAt,
failCount: job.attrs.failCount,
data: job.attrs.data,
})));
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Create new job
app.post('/api/jobs', async (req, res) => {
try {
const { name, schedule, data } = req.body;
if (!name || !schedule) {
return res.status(400).json({ error: 'Missing name or schedule' });
}
const job = agenda.schedule(schedule, name, data);
res.status(201).json({
id: job._id,
message: 'Job scheduled successfully'
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Cancel job
app.delete('/api/jobs/:id', async (req, res) => {
try {
await agenda.cancel({ _id: req.params.id });
res.json({ message: 'Job cancelled' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get job statistics
app.get('/api/stats', async (req, res) => {
try {
const jobs = await agenda.jobs({});
const stats = {
total: jobs.length,
pending: jobs.filter(j => !j.attrs.lastRunAt).length,
failed: jobs.filter(j => j.attrs.failCount > 0).length,
nextRun: jobs
.filter(j => j.attrs.nextRunAt)
.sort((a, b) => a.attrs.nextRunAt - b.attrs.nextRunAt)[0],
};
res.json(stats);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
Tips & Best Practices
1. Choose the Right Tool
| Feature | node-schedule | Agenda |
| Single server | ✓ | ✓ |
| Distributed | ✗ | ✓ |
| Persistence | ✗ | ✓ |
| Scalability | Low | High |
| Complexity | Low | Medium |
| Best for | Dev/testing | Production |
2. Error Handling & Retries
agenda.define('resilient-job', async (job) => {
const maxRetries = 3;
const retryDelay = 5000;
try {
await riskyOperation();
} catch (error) {
if (job.attrs.failCount < maxRetries) {
// Reschedule for retry
job.schedule(new Date(Date.now() + retryDelay));
await job.save();
} else {
// Log to error tracking service
await logToSentry(error, job);
throw error;
}
}
});
3. Timezone Handling
const TZ = 'America/New_York';
// node-schedule with timezone
const rule = new schedule.RecurrenceRule();
rule.hour = 9;
rule.minute = 0;
rule.tz = TZ;
schedule.scheduleJob(rule, () => {
console.log('9 AM in New York');
});
// Agenda with timezone
agenda.every('0 9 * * *', 'daily-task', {}, { tz: TZ });
4. Monitoring & Logging
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'jobs.log' }),
],
});
agenda.on('success', (job) => {
logger.info('Job success', {
name: job.attrs.name,
duration: Date.now() - job.attrs.lastRunAt,
});
});
agenda.on('fail', (error, job) => {
logger.error('Job failed', {
name: job.attrs.name,
error: error.message,
failCount: job.attrs.failCount,
});
});
5. Testing Scheduled Jobs
// test-scheduler.js
const { expect } = require('chai');
const sinon = require('sinon');
const agenda = require('./agenda-setup');
describe('Scheduled Jobs', () => {
it('should execute cleanup job', async () => {
const stub = sinon.stub().resolves();
agenda.define('test-cleanup', stub);
const job = agenda.create('test-cleanup').save();
await job.run();
expect(stub.calledOnce).to.be.true;
});
it('should retry failed jobs', async () => {
const stub = sinon.stub()
.onFirstCall().rejects(new Error('Fail'))
.onSecondCall().resolves();
agenda.define('test-retry', stub);
const job = agenda.create('test-retry').save();
try {
await job.run();
} catch (e) {
// Expected
}
await job.run();
expect(stub.calledTwice).to.be.true;
});
});
6. Performance Optimization
// Tune for your workload
const agenda = new Agenda({
db: { address: mongoConnectionString },
processEvery: '10 seconds', // Check for jobs every 10s
maxConcurrency: 50, // Max concurrent jobs
defaultConcurrency: 5, // Default per job type
lockLimit: 0, // Unlimited locks
lockInterval: 5000, // Lock check interval
defaultLockLimit: 0,
});
// Batch similar jobs
agenda.every('*/5 * * * *', 'batch-process', {
batchSize: 100,
timeout: 30000,
});
7. Production Checklist
- ✓ Use MongoDB with replication for high availability
- ✓ Implement comprehensive error logging
- ✓ Set up monitoring dashboards
- ✓ Configure graceful shutdown handlers
- ✓ Use environment-specific configurations
- ✓ Implement job timeouts
- ✓ Test failure scenarios
- ✓ Document job dependencies
- ✓ Set up alerts for failed jobs
- ✓ Regular backup of job database
Conclusion
node-schedule excels for simple, single-server scenarios with minimal overhead. Agenda is the production choice for distributed systems requiring reliability, persistence, and scalability. Combine them strategically: use node-schedule for lightweight tasks and Agenda for critical, distributed workloads.