NestJS Task Scheduling

Task scheduling runs code automatically at specified times or intervals — without any user triggering a request. Sending weekly report emails, clearing expired tokens every hour, syncing data from an external API every 10 minutes — all of these require scheduled tasks. NestJS provides the @nestjs/schedule package that wraps the node-cron library with a clean, decorator-based API.

Installing the Schedule Package

npm install @nestjs/schedule
npm install -D @types/cron

Enabling the Scheduler

// app.module.ts
import { ScheduleModule } from '@nestjs/schedule';

@Module({
  imports: [ ScheduleModule.forRoot() ],
})
export class AppModule {}

Three Types of Scheduled Tasks

Type       | Decorator    | When it runs
-----------|--------------|-------------------------------
Cron job   | @Cron()      | At a specific time (cron expr)
Interval   | @Interval()  | Every N milliseconds
Timeout    | @Timeout()   | Once, after N milliseconds

Cron Jobs

A cron expression defines a precise schedule using five or six time fields. The six-field format includes seconds:

* * * * * *
| | | | | Day of week (0-7)
| | | | Month (1-12)
| | | Day of month (1-31)
| | Hour (0-23)
| Minute (0-59)
Second (0-59) [optional]

Examples:
  0 * * * * *   - every minute at second 0
  0 0 9 * * *   - every day at 9:00 AM
  0 0 9 * * 1   - every Monday at 9:00 AM
  0 0 0 1 * *   - first day of every month at midnight
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class TasksService {

  @Cron(CronExpression.EVERY_HOUR)
  clearExpiredTokens() {
    this.authService.deleteExpiredTokens();
  }

  @Cron('0 9 * * 1')    // Every Monday at 9 AM
  sendWeeklyReport() {
    this.emailService.sendWeeklyReport();
  }

  @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
  archiveOldLogs() {
    this.logsService.archiveOlderThan(30);
  }
}

Built-In CronExpression Constants

CronExpression.EVERY_SECOND
CronExpression.EVERY_10_SECONDS
CronExpression.EVERY_MINUTE
CronExpression.EVERY_5_MINUTES
CronExpression.EVERY_HOUR
CronExpression.EVERY_DAY_AT_MIDNIGHT
CronExpression.EVERY_DAY_AT_NOON
CronExpression.EVERY_WEEK
CronExpression.EVERY_MONTH

Interval Tasks

An interval task runs every N milliseconds, repeatedly, for the entire application lifetime:

@Interval(10000)    // every 10 seconds
checkApiHealth() {
  this.healthService.ping('https://external-api.com/health');
}

@Interval(60 * 60 * 1000)   // every 1 hour
syncExternalData() {
  this.syncService.pullLatestData();
}

Timeout Tasks

A timeout task runs once after a delay from application startup:

@Timeout(5000)    // 5 seconds after app starts, runs once
sendStartupNotification() {
  this.notificationService.notifyAdmins('Server started');
}

Registering the Task Service

@Module({
  providers: [TasksService],
})
export class TasksModule {}

Dynamic Cron Jobs

The SchedulerRegistry lets you add, remove, and stop cron jobs at runtime — useful when users configure their own notification schedules:

import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob } from 'cron';

@Injectable()
export class DynamicTasksService {
  constructor(private schedulerRegistry: SchedulerRegistry) {}

  addUserJob(userId: string, cronTime: string) {
    const job = new CronJob(cronTime, () => {
      this.sendUserNotification(userId);
    });
    this.schedulerRegistry.addCronJob(`user-${userId}`, job);
    job.start();
  }

  removeUserJob(userId: string) {
    this.schedulerRegistry.deleteCronJob(`user-${userId}`);
  }
}

Task Scheduling Diagram

Application starts
       |
       v
ScheduleModule scans all providers
       |
       v
Finds @Cron, @Interval, @Timeout decorators
       |
       v
Registers each task with the scheduler

Timeline:
  T+5s          → sendStartupNotification() [once]
  T+10s,20s,... → checkApiHealth() [every 10s]
  T+1h,2h,...   → clearExpiredTokens() [every hour]
  Mon 9:00 AM   → sendWeeklyReport() [weekly]

Best Practices for Scheduled Tasks

Keep scheduled task methods short. Each method should call a service that performs the real work — the same pattern as controllers calling services. Long-running tasks must run asynchronously with async/await and handle errors internally so that a single task failure does not affect the rest of the scheduler.

Add logging at the start and end of every scheduled task. In production, you need confirmation that tasks ran, how long they took, and whether any errors occurred. Without logs, a broken nightly cleanup job can go undetected for days. Visibility into scheduled task execution is as important as logging HTTP requests.

Leave a Comment

Your email address will not be published. Required fields are marked *