---
title: "Running cron jobs in NestJS"
description: "@nestjs/schedule is the official way to run a NestJS cron job. Covers the @Cron decorator, CronExpression values, SchedulerRegistry, and silent error failure."
canonical: https://omiid.me/notebook/47/running-cron-jobs-in-nestjs
author: Omid Sayfun
published: 2026-05-09
updated: 2026-08-09
---

# Running cron jobs in NestJS

`setInterval` at the top of a service is the wrong tool for a scheduled job. It doesn't survive process restarts cleanly, doesn't integrate with NestJS's lifecycle, and makes testing a mess. `@nestjs/schedule` is the official NestJS scheduling package, and it takes about a minute to set up.

## Install and register the module

```bash
npm install @nestjs/schedule
```

Register `ScheduleModule` in your root module:

```typescript
import { ScheduleModule } from "@nestjs/schedule";

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

That's it. Now any injectable class can use the scheduling decorators.

## @Cron runs a method on a cron schedule

This is the most common decorator. It takes a cron expression and runs the method on that schedule:

```typescript
import { Injectable, Logger } from "@nestjs/common";
import { Cron, CronExpression } from "@nestjs/schedule";

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

	@Cron("0 0 9 * * 1-5")
	generateDailyReport() {
		this.logger.log("Generating report...");
	}

	@Cron(CronExpression.EVERY_HOUR)
	refreshCache() {
		this.logger.log("Refreshing cache...");
	}
}
```

NestJS uses **6-field** cron expressions. The first field is seconds, which is different from the standard 5-field format you might know from Unix cron:

```
* * * * * *
│ │ │ │ │ └── day of week (0-7, 0 and 7 = Sunday)
│ │ │ │ └──── month (1-12)
│ │ │ └────── day of month (1-31)
│ │ └──────── hour (0-23)
│ └────────── minute (0-59)
└──────────── second (0-59)
```

Common patterns:

| Expression       | Runs                  |
| ---------------- | --------------------- |
| `0 * * * * *`    | every minute          |
| `0 0 * * * *`    | every hour            |
| `0 0 0 * * *`    | every day at midnight |
| `0 0 9 * * 1-5`  | weekdays at 9am       |
| `0 */3 * * * *`  | every 3 minutes       |
| `0 */5 * * * *`  | every 5 minutes       |
| `0 */15 * * * *` | every 15 minutes      |

If you want to build or validate an expression interactively, the [cron expression tool](https://omiid.me/tools/cron) lets you do that without leaving the site.

If you don't want to memorise the syntax at all, `CronExpression` is an enum with the common cases already defined:

```typescript
import { CronExpression } from "@nestjs/schedule";

CronExpression.EVERY_SECOND;
CronExpression.EVERY_MINUTE;
CronExpression.EVERY_5_MINUTES;
CronExpression.EVERY_30_MINUTES;
CronExpression.EVERY_HOUR;
CronExpression.EVERY_DAY_AT_MIDNIGHT;
CronExpression.EVERY_WEEK;
// ...and more
```

The enum only covers round intervals. There is no value for every 3 minutes, so pass the raw expression `0 */3 * * * *` instead.

The decorator also accepts a `name` and a `timeZone` in its options object:

```typescript
@Cron('0 0 9 * * 1-5', {
  name: 'daily-report',
  timeZone: 'Europe/London',
})
generateDailyReport() { ... }
```

The `name` is required if you want to control the job later via `SchedulerRegistry`.

## @Interval and @Timeout schedule in milliseconds

Use these two when you want millisecond-based scheduling rather than cron expressions:

```typescript
import { Interval, Timeout } from "@nestjs/schedule";

@Injectable()
export class PollingService {
	@Interval(5000)
	pollExternalApi() {
		// runs every 5 seconds
	}

	@Timeout(3000)
	runOnStartup() {
		// runs once, 3 seconds after the app starts
	}
}
```

`@Timeout` is useful when you want to run something shortly after startup (warming a cache, syncing initial state) without blocking the bootstrap process.

## SchedulerRegistry adds and removes cron jobs at runtime

The decorators above only cover schedules you know at build time. `SchedulerRegistry` is the injectable service that registers a dynamic cron job, then stops or removes it while the app is running.

```typescript
import { Injectable } from "@nestjs/common";
import { SchedulerRegistry } from "@nestjs/schedule";
import { CronJob } from "cron";

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

	enableJob(name: string, cronExpression: string, callback: () => void) {
		const job = new CronJob(cronExpression, callback);
		this.schedulerRegistry.addCronJob(name, job);
		job.start();
	}

	disableJob(name: string) {
		const job = this.schedulerRegistry.getCronJob(name);
		job.stop();
	}

	removeJob(name: string) {
		this.schedulerRegistry.deleteCronJob(name);
	}
}
```

This is useful when a job should only run under certain conditions: a feature flag is on, a tenant has enabled it, or an admin toggles it via an API endpoint. You can also list all registered jobs:

```typescript
const jobs = this.schedulerRegistry.getCronJobs();
jobs.forEach((job, name) => {
	console.log(name, job.running);
});
```

## Errors inside a cron handler are swallowed silently

Errors thrown inside a `@Cron` handler are **silently swallowed**. They don't reach NestJS exception filters. They don't show up in your logs. The job just fails and the next tick fires like nothing happened.

```typescript
@Cron(CronExpression.EVERY_HOUR)
async refreshCache() {
  // if this throws, you'll never know
  await this.cache.refresh();
}
```

Wrap every job body in a try/catch and log failures explicitly:

```typescript
@Cron(CronExpression.EVERY_HOUR)
async refreshCache() {
  try {
    await this.cache.refresh();
  } catch (err) {
    this.logger.error('Cache refresh failed', err);
  }
}
```

A cron job that is not running at all is a different problem: `ScheduleModule.forRoot()` was never registered, or the service is not a provider in an imported module. On multiple instances, every replica fires its own copy of the schedule unless you add a lock.

If you're running jobs in production and want visibility, Sentry supports NestJS cron monitoring directly. It tracks whether a job ran, how long it took, and alerts on failures. It is a paid service, so a log line plus your existing alerting is often enough.

## Keep ScheduleModule out of your test module

Scheduled jobs registered via decorators will start firing as soon as the test module boots. This causes test noise, timing issues, and occasionally flaky tests.

The simplest fix is to leave `ScheduleModule` out of your test module. Without it, the decorators are inert, so the methods exist but nothing schedules them.

```typescript
const module = await Test.createTestingModule({
	providers: [ReportService],
	// ScheduleModule intentionally omitted
}).compile();
```

If you need to test job behaviour specifically, or your service depends on `SchedulerRegistry`, mock it:

```typescript
const module = await Test.createTestingModule({
	providers: [
		ReportService,
		{ provide: SchedulerRegistry, useValue: { addCronJob: jest.fn() } },
	],
}).compile();
```

For more on structuring NestJS tests, [this covers the patterns for unit and integration testing](https://omiid.me/notebook/18/unit-and-e2e-testing-in-nestjs-with-jest-and-supertest).
