Scheduled tasks with @nestjs/schedule
Every production application eventually needs something to run on a schedule—cleaning up expired data, sending digest emails, syncing external APIs, or generating reports. In NestJS, the @nestjs/schedule package handles this elegantly without requiring a separate job queue or external orchestration tool. Today we're covering how to add declarative cron jobs, intervals, and dynamic scheduling directly into your application.
Why you need scheduled tasks
I've built several platforms where cron jobs were critical to system health. In one e-commerce system, we cleaned up abandoned cart sessions every night. In a travel platform, we synced hotel availability every 30 minutes. In a trading app, we calculated daily settlements. Without proper scheduling, you either hire a DevOps team to manage separate cron services or you lose data integrity.
The traditional approach was to spin up a separate service—maybe a scheduled Lambda, a Jenkins job, or a standalone Node script running under Supervisor. But @nestjs/schedule lets you keep everything in one place: your NestJS application itself becomes the scheduler.
Setting up @nestjs/schedule
First, install the package:
npm install @nestjs/scheduleThen import ScheduleModule into your root module:
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { SessionsService } from './sessions/sessions.service';
@Module({
imports: [ScheduleModule.forRoot()],
providers: [SessionsService],
})
export class AppModule {}That's it. Now any service in your application can use scheduling decorators.
Declarative cron—the most common pattern
The cleanest way to schedule work is the @Cron decorator. You decorate a method, give it a cron expression, and NestJS handles the rest. Here's a real example from one of my projects:
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { SessionsRepository } from './sessions.repository';
import { Logger } from '@nestjs/common';
@Injectable()
export class SessionsService {
private readonly logger = new Logger(SessionsService.name);
constructor(private readonly sessionsRepo: SessionsRepository) {}
@Cron(CronExpression.EVERY_DAY_AT_2AM)
async cleanupExpiredSessions() {
this.logger.log('Starting expired session cleanup...');
const deletedCount = await this.sessionsRepo.deleteExpired();
this.logger.log(`Deleted ${deletedCount} expired sessions`);
}
}When your application starts, NestJS automatically registers this cron job. At 2 AM every day, the cleanupExpiredSessions method runs automatically. No manual scheduling, no external services, no config files.
NestJS provides convenient expressions like EVERY_DAY_AT_2AM, but you can also write raw cron syntax:
// Run every 15 minutes
@Cron('0 */15 * * * *')
async syncExternalApi() {
// ...
}
// Run every Monday at 9 AM
@Cron('0 9 * * 1')
async weeklyReport() {
// ...
}The cron expression format is the standard Linux/Unix format: second minute hour day month dayOfWeek. If you're not comfortable writing cron expressions, use the predefined constants or an online cron generator.
Intervals and timeouts for simpler recurring work
Not all scheduled work needs to be time-based. Sometimes you just want something to run every N milliseconds, or once after a delay. That's where @Interval and @Timeout come in.
I used @Interval in a ride-hailing app to check for stale driver locations every 5 seconds:
import { Injectable } from '@nestjs/common';
import { Interval, Timeout } from '@nestjs/schedule';
@Injectable()
export class DriverLocationService {
@Interval(5000) // Run every 5 seconds (milliseconds)
async cleanupStaleLocations() {
const removed = await this.db.drivers
.deleteMany({
lastSeenAt: { $lt: new Date(Date.now() - 30000) },
});
console.log(`Removed ${removed.deletedCount} stale locations`);
}
}
And @Timeout runs once after the application boots, with a delay:
@Injectable()
export class HealthCheckService {
@Timeout(5000) // Run once, 5 seconds after boot
async warmupDatabaseConnection() {
console.log('Warming up DB connection pool...');
await this.db.query('SELECT 1');
}
}
@Interval is useful for lightweight health checks, cache refreshes, or any recurring task that doesn't need precise timing. @Timeout is perfect for initialization tasks that should happen shortly after your app is ready.
Dynamic jobs with SchedulerRegistry
Sometimes you need to add, remove, or modify jobs at runtime. A SaaS platform might let users customize their notification schedule. A trading system might add new market sync jobs based on user subscription. That's where SchedulerRegistry shines.
import { Injectable, Logger } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob } from 'cron';
@Injectable()
export class NotificationService {
private readonly logger = new Logger(NotificationService.name);
constructor(private readonly scheduler: SchedulerRegistry) {}
// Add a dynamic cron job
addDailyNotification(userId: string, hour: number, minute: number) {
const jobName = `daily-notification-${userId}`;
// Check if job already exists
try {
this.scheduler.getCronJob(jobName);
this.logger.warn(`Job ${jobName} already exists`);
return;
} catch (error) {
// Job doesn't exist, continue
}
const cronExpression = `0 ${minute} ${hour} * * *`;
const job = new CronJob(cronExpression, () => {
this.logger.log(`Sending notification to user ${userId}`);
this.sendNotification(userId);
});
this.scheduler.addCronJob(jobName, job);
job.start();
this.logger.log(`Added cron job: ${jobName}`);
}
// Remove a dynamic job
removeDailyNotification(userId: string) {
const jobName = `daily-notification-${userId}`;
try {
const job = this.scheduler.getCronJob(jobName);
job.stop();
this.scheduler.deleteCronJob(jobName);
this.logger.log(`Removed cron job: ${jobName}`);
} catch (error) {
this.logger.warn(`Job ${jobName} not found`);
}
}
// List all active cron jobs
listAllJobs() {
const jobs = this.scheduler.getCronJobs();
jobs.forEach((job, name) => {
console.log(`${name}: ${job.nextDate()}`);
});
}
private sendNotification(userId: string) {
// Actual notification logic
}
}
You can also add dynamic intervals:
const jobName = `sync-market-${marketId}`;
const callback = () => this.syncMarketData(marketId);
this.scheduler.addInterval(jobName, 30000, callback); // Every 30 seconds
// Later, remove it
this.scheduler.deleteInterval(jobName);Use SchedulerRegistry when your scheduling logic is data-driven—when the schedule depends on user preferences, configuration, or runtime decisions.
Common mistakes
- Running jobs on every replica. If you deploy three instances of your NestJS app, every scheduled task runs on all three simultaneously. You'll send three copies of the same email or process the same data three times. We'll solve this next post with distributed locking.
- Forgetting to handle errors. If your cron job throws, it might silently fail or crash your application. Always wrap async work in try-catch and log properly.
- Long-running tasks blocking other work. A cron job that takes 20 minutes will hold a thread and may delay other scheduled tasks. For heavy work, consider a separate queue (Bull, RabbitMQ) instead.
- Cron expressions that don't do what you think. Testing cron syntax is easy to get wrong. Use an online validator or hardcode known expressions like
CronExpression.EVERY_DAY_AT_2AM. - Not logging job execution. Six months later, you won't remember which jobs exist or when they ran. Always log start and end times, and especially log errors.
Key takeaways
@Cronis your go-to for time-based scheduling; use predefined expressions when possible to avoid syntax errors.@Intervaland@Timeouthandle millisecond-based scheduling for simpler recurring tasks.SchedulerRegistrylets you add, remove, and manage jobs at runtime for data-driven scheduling scenarios.- Every replica runs every cron job by default—you'll need distributed locking or a single scheduler instance for production multi-instance deployments.
- Always log job execution and handle errors gracefully; silent failures in background tasks are nightmare fuel.






Comments
Loading comments…