Skip to content

Scheduler

@veloxts/scheduler provides expressive, fluent task scheduling with cron expressions.

Terminal window
pnpm add @veloxts/scheduler
import { schedulerPlugin, task } from '@veloxts/scheduler';
app.register(schedulerPlugin({
timezone: 'UTC',
tasks: [
task('cleanup-sessions', async (ctx) => {
await ctx.db.session.deleteMany({
where: { expiresAt: { lt: new Date() } },
});
})
.daily()
.at('03:00')
.build(),
],
}));
task('name', handler)
// Time-based
.everyMinute()
.everyFiveMinutes()
.everyFifteenMinutes()
.everyThirtyMinutes()
.hourly()
.hourlyAt(15) // At :15 past the hour
.daily()
.dailyAt('09:00')
.weekly()
.weeklyOn('monday', '09:00')
.monthly()
.monthlyOn(1, '00:00') // 1st of month at midnight
// Day constraints
.weekdays() // Monday-Friday
.weekends() // Saturday-Sunday
// Custom cron
.cron('*/5 * * * *') // Every 5 minutes
.build()
task('sync-data', syncData)
.hourly()
.timezone('America/New_York') // Task-specific timezone
.withoutOverlapping() // Skip if still running
.withoutOverlapping(30) // Max lock time in minutes
.when(() => isMainServer()) // Conditional execution
.skip(() => isMaintenanceMode()) // Skip condition
.onSuccess((ctx, duration) => {
console.log(`Completed in ${duration}ms`);
})
.onFailure((ctx, error, duration) => {
notifySlack(`Task failed: ${error.message}`);
})
.build()
// Access scheduler from context
const scheduler = ctx.scheduler;
// Check status
scheduler.isRunning();
// Get tasks
const tasks = scheduler.getTasks();
const task = scheduler.getTask('cleanup-tokens');
// Manual execution
await scheduler.runTask('cleanup-tokens');
// Next run time
const nextRun = scheduler.getNextRun('cleanup-tokens');

Unlike other ecosystem packages, the scheduler doesn’t require Redis. However, it must only run on one server instance to prevent duplicate task execution.

ScenarioResult
3 instances, all with schedulerEach task runs 3x
3 instances, 1 with schedulerEach task runs 1x ✓
src/index.ts
app.register(schedulerPlugin({
timezone: process.env.SCHEDULER_TIMEZONE || 'UTC',
tasks: [
task('cleanup', cleanup)
.daily()
.at('02:00')
.when(() => process.env.SCHEDULER_ENABLED === 'true')
.withoutOverlapping(10)
.build(),
],
onTaskStart: (task, ctx) => {
console.log(`Starting: ${task.name}`);
},
onTaskComplete: (task, ctx, duration) => {
metrics.timing(`scheduler.${task.name}`, duration);
},
onTaskError: (task, ctx, error) => {
Sentry.captureException(error, { tags: { task: task.name } });
},
}));
.env
SCHEDULER_ENABLED=true # Only set on scheduler instance
SCHEDULER_TIMEZONE=UTC # Default timezone

Allow running tasks to complete before shutdown:

src/index.ts
process.on('SIGTERM', async () => {
await app.close(); // Waits for scheduler to stop
process.exit(0);
});

For better isolation, run scheduler separately from your web server:

scheduler.ts
import { createScheduler, task } from '@veloxts/scheduler';
const scheduler = createScheduler({
timezone: process.env.SCHEDULER_TIMEZONE || 'UTC',
tasks: [
task('cleanup', cleanup).daily().at('02:00').build(),
task('reports', sendReports).weekdays().at('09:00').build(),
],
});
scheduler.start();
process.on('SIGTERM', async () => {
await scheduler.stop();
process.exit(0);
});
Terminal window
# Run as separate process
node scheduler.js
  1. Single instance - Enable scheduler on ONE server only
  2. Graceful shutdown - Handle SIGTERM to let tasks complete
  3. Error monitoring - Track failures with onTaskError
  4. Overlap prevention - Use withoutOverlapping() for long tasks
  5. Explicit timezone - Set timezone for predictable execution

Use scheduler outside of Fastify context:

import { createScheduler, task } from '@veloxts/scheduler';
const scheduler = createScheduler({
timezone: 'UTC',
tasks: [
task('my-task', () => console.log('Running!'))
.everyMinute()
.build(),
],
});
scheduler.start();
// Later...
await scheduler.stop();