Task scheduling
Task scheduling lets you run arbitrary code (methods or functions) at a fixed date and time, at recurring intervals, or once after a specified delay. In the Linux world, this is often handled at the OS level by tools like cron. For Node.js apps, several packages emulate cron-like functionality. Nest provides the @nestjs/schedule package, which integrates with the popular Node.js cron package. This chapter covers the @nestjs/schedule package.
Installation#
To begin using it, install the required dependencies.
$ npm install --save @nestjs/schedule
To activate job scheduling, import the ScheduleModule into the root AppModule and call the forRoot() static method, as shown below:
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [
ScheduleModule.forRoot()
],
})
export class AppModule {}
The forRoot() call initializes the scheduler and registers any declarative cron jobs, timeouts, and intervals that exist within your app. The scheduled jobs start in the onApplicationBootstrap lifecycle hook, which ensures that all modules have loaded and declared their scheduled jobs.
Warning CallforRoot()in one module only. Each additional import registers every@Cron(),@Interval()and@Timeout()handler in your app again, so importing it in three modules makes each job run three times.
Declarative cron jobs#
A cron job schedules an arbitrary function (method call) to run automatically. Cron jobs can run:
- Once, at a specified date and time.
- On a recurring basis. Recurring jobs can run at a specified instant within a specified interval (e.g., once per hour, once per week, once every 5 minutes).
Declare a cron job by applying the @Cron() decorator to the method that contains the code to execute, as follows:
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
@Cron('45 * * * * *')
handleCron() {
this.logger.debug('Called when the current second is 45');
}
}
In this example, the handleCron() method is called each time the current second is 45. In other words, the method runs once per minute, at the 45-second mark.
The @Cron() decorator supports the following standard cron patterns:
- Asterisks (e.g.,
*) - Ranges and lists (e.g.,
1-3,5) - Steps (e.g.,
*/2)
In the example above, we passed 45 * * * * * to the decorator. The following key shows how each position in the cron pattern string is interpreted:
* * * * * *
| | | | | |
| | | | | day of week
| | | | months
| | | day of month
| | hours
| minutes
seconds (optional)
Some sample cron patterns are:
* * * * * * | every second |
45 * * * * * | every minute, on the 45th second |
0 10 * * * * | every hour, at the start of the 10th minute |
0 */30 9-17 * * * | every 30 minutes between 9am and 5pm |
0 30 11 * * 1-5 | Monday to Friday at 11:30am |
The @nestjs/schedule package provides the CronExpression enum with commonly used cron patterns. You can use this enum as follows:
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_30_SECONDS)
handleCron() {
this.logger.debug('Called every 30 seconds');
}
}
In this example, the handleCron() method is called every 30 seconds. Every method annotated with @Cron() is automatically wrapped in a try-catch block, so if an exception occurs, it's logged to the console.
Alternatively, you can supply a JavaScript Date object to the @Cron() decorator. This causes the job to execute exactly once, at the specified date.
Hint Use JavaScript date arithmetic to schedule jobs relative to the current date. For example, @Cron(new Date(Date.now() + 10 * 1000)) schedules a job to run 10 seconds after the app starts.
You can also supply additional options as the second parameter to the @Cron() decorator.
name | Lets you access and control a cron job after it's been declared. |
timeZone | Specifies the time zone for the execution. This modifies the actual time relative to your time zone. If the time zone is invalid, an error is thrown. You can check all available time zones on the Moment Timezone website. |
utcOffset | Specifies the offset of your time zone instead of using the timeZone option. Don't combine it with timeZone. |
waitForCompletion | If true, no additional instances of the cron job run until the current onTick callback completes. Any new scheduled executions that occur while the current cron job is running are skipped entirely. |
disabled | If true, the job is not executed at all (defaults to false). |
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class NotificationService {
@Cron('* * 0 * * *', {
name: 'notifications',
timeZone: 'Europe/Paris',
})
triggerNotifications() {}
}
With the dynamic API, you can access and control a cron job after it's been declared, or create a cron job dynamically (with its cron pattern defined at runtime). To access a declarative cron job via the API, you must associate the job with a name by passing the name property in the options object (the decorator's optional second argument).
Declarative intervals#
To declare that a method should run at a specified (recurring) interval, prefix the method definition with the @Interval() decorator. Pass the interval value to the decorator as a number in milliseconds, as shown below:
@Interval(10000)
handleInterval() {
this.logger.debug('Called every 10 seconds');
}
Hint This mechanism uses the JavaScript setInterval() function under the hood. You can also use a cron job to schedule recurring jobs.
To control your declarative interval from outside the declaring class via the dynamic API, associate the interval with a name using the following construction:
@Interval('notifications', 2500)
handleInterval() {}
Every method annotated with @Interval() is automatically wrapped in a try-catch block, so if an exception occurs, it's logged to the console.
The dynamic API also enables creating dynamic intervals, where the interval's properties are defined at runtime, and listing and deleting them.
Declarative timeouts#
To declare that a method should run once after a specified timeout, prefix the method definition with the @Timeout() decorator. Pass the time offset from application startup (in milliseconds) to the decorator, as shown below:
@Timeout(5000)
handleTimeout() {
this.logger.debug('Called once after 5 seconds');
}
Hint This mechanism uses the JavaScript setTimeout() function under the hood.
Every method annotated with @Timeout() is automatically wrapped in a try-catch block, so if an exception occurs, it's logged to the console.
To control your declarative timeout from outside the declaring class via the dynamic API, associate the timeout with a name using the following construction:
@Timeout('notifications', 2500)
handleTimeout() {}
The dynamic API also enables creating dynamic timeouts, where the timeout's properties are defined at runtime, and listing and deleting them.
Dynamic schedule module API#
The @nestjs/schedule module provides a dynamic API for managing declarative cron jobs, timeouts, and intervals. The API also lets you create and manage dynamic cron jobs, timeouts, and intervals, whose properties are defined at runtime.
Dynamic cron jobs#
Obtain a reference to a CronJob instance by name from anywhere in your code using the SchedulerRegistry API. First, inject SchedulerRegistry using standard constructor injection:
constructor(private schedulerRegistry: SchedulerRegistry) {}
Hint Import theSchedulerRegistryfrom the@nestjs/schedulepackage.
Then use it in a class. Assume a cron job was created with the following declaration:
@Cron('* * 8 * * *', {
name: 'notifications',
})
triggerNotifications() {}
Access this job using the following:
const job = this.schedulerRegistry.getCronJob('notifications');
job.stop();
console.log(job.lastDate());
The getCronJob() method returns the named cron job. The returned CronJob object has the following methods:
stop()- stops a job that is scheduled to run.start()- restarts a job that has been stopped.setTime(time: CronTime)- stops a job, sets a new time for it, and then starts it.lastDate()- returns a JavaScriptDaterepresenting the last execution of the job, ornullif it hasn't run yet.nextDate()- returns aDateTimerepresentation of the date when the next execution of the job is scheduled.nextDates(count: number)- returns an array (of sizecount) ofDateTimerepresentations for the next set of dates that will trigger job execution.countdefaults to 0, which returns an empty array.
Hint CalltoJSDate()on aDateTimeobject (a Luxon type) to convert it to the equivalent JavaScriptDate.
Create a new cron job dynamically using the SchedulerRegistry#addCronJob method, as follows:
addCronJob(name: string, seconds: string) {
const job = new CronJob(`${seconds} * * * * *`, () => {
this.logger.warn(`time (${seconds}) for job ${name} to run!`);
});
this.schedulerRegistry.addCronJob(name, job);
job.start();
this.logger.warn(
`job ${name} added for each minute at ${seconds} seconds!`,
);
}
This code uses the CronJob class from the cron package to create the cron job. The CronJob constructor takes a cron pattern (like the @Cron()decorator) as its first argument, and a callback to execute when the cron timer fires as its second argument. The SchedulerRegistry#addCronJob method takes two arguments: a name for the CronJob, and the CronJob object itself.
Warning Remember to inject theSchedulerRegistrybefore accessing it. ImportCronJobfrom thecronpackage.
Delete a named cron job using the SchedulerRegistry#deleteCronJob method, as follows:
deleteCron(name: string) {
this.schedulerRegistry.deleteCronJob(name);
this.logger.warn(`job ${name} deleted!`);
}
List all cron jobs using the SchedulerRegistry#getCronJobs method, as follows:
getCrons() {
const jobs = this.schedulerRegistry.getCronJobs();
jobs.forEach((value, key, map) => {
let next;
try {
next = value.nextDate().toJSDate();
} catch (e) {
next = 'error: next fire date is in the past!';
}
this.logger.log(`job: ${key} -> next: ${next}`);
});
}
The getCronJobs() method returns a Map. This code iterates over the map and calls the nextDate() method of each CronJob. In the CronJob API, nextDate() throws an exception if a job has already fired and has no future firing date.
Dynamic intervals#
Obtain a reference to an interval with the SchedulerRegistry#getInterval method. As above, inject SchedulerRegistry using standard constructor injection:
constructor(private schedulerRegistry: SchedulerRegistry) {}
Then use it as follows:
const interval = this.schedulerRegistry.getInterval('notifications');
clearInterval(interval);
Create a new interval dynamically using the SchedulerRegistry#addInterval method, as follows:
addInterval(name: string, milliseconds: number) {
const callback = () => {
this.logger.warn(`Interval ${name} executing at time (${milliseconds})!`);
};
const interval = setInterval(callback, milliseconds);
this.schedulerRegistry.addInterval(name, interval);
}
This code creates a standard JavaScript interval, then passes it to the SchedulerRegistry#addInterval method.
That method takes two arguments: a name for the interval, and the interval itself.
Delete a named interval using the SchedulerRegistry#deleteInterval method, as follows:
deleteInterval(name: string) {
this.schedulerRegistry.deleteInterval(name);
this.logger.warn(`Interval ${name} deleted!`);
}
List the names of all intervals using the SchedulerRegistry#getIntervals method, as follows:
getIntervals() {
const intervals = this.schedulerRegistry.getIntervals();
intervals.forEach(key => this.logger.log(`Interval: ${key}`));
}
Dynamic timeouts#
Obtain a reference to a timeout with the SchedulerRegistry#getTimeout method. As above, inject SchedulerRegistry using standard constructor injection:
constructor(private readonly schedulerRegistry: SchedulerRegistry) {}
Then use it as follows:
const timeout = this.schedulerRegistry.getTimeout('notifications');
clearTimeout(timeout);
Create a new timeout dynamically using the SchedulerRegistry#addTimeout method, as follows:
addTimeout(name: string, milliseconds: number) {
const callback = () => {
this.logger.warn(`Timeout ${name} executing after (${milliseconds})!`);
};
const timeout = setTimeout(callback, milliseconds);
this.schedulerRegistry.addTimeout(name, timeout);
}
This code creates a standard JavaScript timeout, then passes it to the SchedulerRegistry#addTimeout method.
That method takes two arguments: a name for the timeout, and the timeout itself.
Delete a named timeout using the SchedulerRegistry#deleteTimeout method, as follows:
deleteTimeout(name: string) {
this.schedulerRegistry.deleteTimeout(name);
this.logger.warn(`Timeout ${name} deleted!`);
}
List the names of all timeouts using the SchedulerRegistry#getTimeouts method, as follows:
getTimeouts() {
const timeouts = this.schedulerRegistry.getTimeouts();
timeouts.forEach(key => this.logger.log(`Timeout: ${key}`));
}
Knowing a cron job actually ran#
A scheduled job that throws is a problem you will hear about. A scheduled job that silently stops being scheduled (the process crashed, the container was descheduled, a deploy shipped a @Cron() expression with a typo) is a problem nobody hears about until the report it generates is missing.
This is the failure mode cron monitoring exists for, and NestJS Observe covers it without a third-party ping service or a heartbeat URL for the job to call on its way out. Scheduled runs are reported as jobs, alongside queue consumers, each with its duration, outcome, and failure reason. A job silence alert rule fires when a named job hasn't reported within a tolerance you set, anywhere from 2 minutes to 7 days: "alert me if daily-invoices has not reported in the last 26 hours".
Two details make this practical rather than noisy. A scope that has never reported anything doesn't fire, so you can add the rule before the job ships without being paged for something that isn't integrated yet. And rules carry a recurring mute schedule, so a job that legitimately doesn't run at weekends doesn't wake anyone on Saturday.
A handler that needs to report its own progress can inject TracerService and record spans or custom metrics from inside the run. Metrics in particular aren't tied to a trace, so they can be reported from cron jobs and lifecycle hooks alike. See Manual instrumentation for that, and the Observability chapter for setup.
Example#
A working example is available in the 27-scheduling sample.

