Skip to content
NestJS ns tasks 4 min read

Task Scheduling with Cron

Most real-world applications need to do work on a schedule: clearing expired sessions every night, polling an external API every few minutes, or sending a one-off warm-up request shortly after boot. NestJS ships this capability through the @nestjs/schedule package, which wraps the battle-tested cron library and exposes it as clean, declarative decorators. Instead of wiring up timers by hand, you annotate provider methods and let the framework manage the lifecycle for you.

Installing and registering the module

The scheduling feature lives in a separate package. Install it alongside its peer dependency types and import ScheduleModule.forRoot() once, typically in your root module. Calling forRoot() bootstraps the scheduler and registers every declared job once the application has fully started.

npm install @nestjs/schedule
// app.module.ts
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { TasksService } from './tasks.service';

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

You only call ScheduleModule.forRoot() a single time in the root module. The decorators are discovered automatically across all providers in the application, so feature modules just need their providers registered normally.

Declarative cron jobs with @Cron

The @Cron decorator runs a method on a standard cron schedule. You can pass a raw cron string or, better, one of the CronExpression presets, which keep your intent readable and free of off-by-one mistakes.

// tasks.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class TasksService {
  private readonly logger = new Logger(TasksService.name);

  @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
  handleNightlyCleanup() {
    this.logger.log('Running nightly cleanup');
  }

  // Raw cron syntax: at second 45 of every minute
  @Cron('45 * * * * *')
  handleEveryMinuteAt45s() {
    this.logger.debug('Triggered at the 45-second mark');
  }
}

Output:

[Nest] 4821  - 06/14/2026, 12:00:00 AM   LOG [TasksService] Running nightly cleanup
[Nest] 4821  - 06/14/2026, 12:01:45 AM DEBUG [TasksService] Triggered at the 45-second mark

NestJS cron expressions use six fields (with an optional leading seconds field): second minute hour day-of-month month day-of-week. The presets cover the common cases:

PresetCron equivalentFires
EVERY_10_SECONDS*/10 * * * * *Every 10 seconds
EVERY_MINUTE* * * * *Top of every minute
EVERY_HOUR0 0-23/1 * * *Top of every hour
EVERY_DAY_AT_MIDNIGHT0 0 * * *00:00 daily
EVERY_WEEK0 0 * * 0Sunday at midnight

Intervals and timeouts

When you only need fixed spacing rather than calendar alignment, @Interval and @Timeout are simpler than crafting a cron string. @Interval repeats every N milliseconds; @Timeout fires exactly once, N milliseconds after the app starts.

import { Injectable, Logger } from '@nestjs/common';
import { Interval, Timeout } from '@nestjs/schedule';

@Injectable()
export class HeartbeatService {
  private readonly logger = new Logger(HeartbeatService.name);

  @Interval(10_000)
  pingHealthCheck() {
    this.logger.log('Heartbeat ping');
  }

  @Timeout(5_000)
  warmUpCache() {
    this.logger.log('Warming up cache after startup');
  }
}
DecoratorArgumentBehaviour
@Croncron string / CronExpressionRuns on a calendar schedule
@IntervalmillisecondsRepeats at a fixed delay
@TimeoutmillisecondsRuns once after the delay

Time zones

Cron jobs run in the server’s local time zone by default, which is fragile across deployments. Pass an options object with a timeZone (IANA identifier) to pin the schedule deterministically, so a “midnight” job means midnight where your business operates.

@Cron('0 0 * * *', {
  name: 'eu-daily-report',
  timeZone: 'Europe/Paris',
})
generateDailyReport() {
  this.logger.log('Generating Paris-time daily report');
}

Always set an explicit timeZone for business-critical jobs. Relying on server local time leads to silent shifts when you move from a laptop to a cloud region in UTC, and breaks again twice a year around daylight saving transitions.

Named jobs

Giving a job a name lets you retrieve it later from the SchedulerRegistry to inspect, pause, or stop it at runtime. This is the foundation for dynamic scheduling.

import { Injectable, Logger } from '@nestjs/common';
import { Cron, SchedulerRegistry } from '@nestjs/schedule';

@Injectable()
export class ReportService {
  private readonly logger = new Logger(ReportService.name);

  constructor(private readonly schedulerRegistry: SchedulerRegistry) {}

  @Cron('0 * * * *', { name: 'hourly-report' })
  hourlyReport() {
    this.logger.log('Hourly report generated');
  }

  stopReports() {
    const job = this.schedulerRegistry.getCronJob('hourly-report');
    job.stop();
    this.logger.warn('Hourly report job stopped');
  }
}

Output:

[Nest] 4821  - 06/14/2026, 1:00:00 PM   LOG [ReportService] Hourly report generated
[Nest] 4821  - 06/14/2026, 1:05:12 PM  WARN [ReportService] Hourly report job stopped

Best Practices

  • Import ScheduleModule.forRoot() exactly once in the root module and register task providers in their own feature modules.
  • Prefer CronExpression presets over raw strings for readability, falling back to raw cron only when no preset fits.
  • Always set an explicit timeZone on schedule-sensitive jobs rather than trusting server local time.
  • Name long-lived jobs so you can manage them through the SchedulerRegistry later.
  • Keep handler bodies thin: enqueue heavy work to a queue (e.g. BullMQ) instead of doing CPU-bound processing inside the scheduler tick.
  • Guard against overlapping runs for slow jobs, since the scheduler will still fire on the next tick even if the previous run has not finished.
  • Wrap fallible work in try/catch and log failures, because an unhandled rejection inside a cron handler will not stop the schedule but can crash the process.
Last updated June 14, 2026
Was this helpful?