Dynamic Scheduling
Declarative @Cron and @Interval decorators are perfect for jobs you know at compile time, but real applications often need to schedule work in response to runtime data — a user-defined reminder, a tenant-specific report, or a cleanup task whose timing comes from a database. NestJS exposes the SchedulerRegistry, a runtime registry that lets you add, inspect, and remove cron jobs, intervals, and timeouts on the fly. This page shows how to drive scheduling programmatically with fully type-safe APIs.
The SchedulerRegistry
SchedulerRegistry is provided by ScheduleModule and injected like any other provider. It keeps named references to every job the scheduler owns — both decorator-defined and dynamically created — and gives you methods to mutate that set while the app is running.
import { Injectable } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
@Injectable()
export class JobsService {
constructor(private readonly registry: SchedulerRegistry) {}
}
Make sure ScheduleModule.forRoot() is imported once in your root module so the registry and the underlying timers are bootstrapped.
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { JobsService } from './jobs.service';
@Module({
imports: [ScheduleModule.forRoot()],
providers: [JobsService],
})
export class AppModule {}
Adding a cron job at runtime
To create a cron job dynamically, build a CronJob instance (from the cron package that NestJS re-exports) and register it under a unique name. The registry only stores the job — you must call start() to begin firing it.
import { Injectable, Logger } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob } from 'cron';
@Injectable()
export class JobsService {
private readonly logger = new Logger(JobsService.name);
constructor(private readonly registry: SchedulerRegistry) {}
addCronJob(name: string, cronTime: string) {
const job = new CronJob(cronTime, () => {
this.logger.log(`Job "${name}" fired at ${new Date().toISOString()}`);
});
this.registry.addCronJob(name, job);
job.start();
this.logger.log(`Added cron job "${name}" with schedule ${cronTime}`);
}
}
Output:
[Nest] LOG [JobsService] Added cron job "report-tenant-42" with schedule 0 */5 * * * *
[Nest] LOG [JobsService] Job "report-tenant-42" fired at 2026-06-14T10:05:00.012Z
Job names must be unique. Calling
addCronJobwith a name that already exists throws an error, so guard withregistry.doesExist('cron', name)if a collision is possible.
Querying registered jobs
The registry can return individual jobs by name or enumerate the whole set. Use these to build a status endpoint or to compute the next run time for a UI.
getCronJob(name: string) {
const job = this.registry.getCronJob(name);
const next = job.nextDate().toJSDate();
return { running: job.isCallbackRunning ?? job.running, next };
}
listCronJobs() {
const jobs = this.registry.getCronJobs(); // Map<string, CronJob>
return [...jobs.entries()].map(([name, job]) => {
let next: string;
try {
next = job.nextDate().toISO();
} catch {
next = 'stopped';
}
return { name, next };
});
}
Output:
[
{ "name": "report-tenant-42", "next": "2026-06-14T10:10:00.000Z" },
{ "name": "cleanup-temp", "next": "2026-06-15T00:00:00.000Z" }
]
Deleting a cron job
Removing a job both stops it and drops it from the registry. Always delete jobs you create dynamically when their owning entity goes away, or you will leak timers.
deleteCronJob(name: string) {
this.registry.deleteCronJob(name);
this.logger.warn(`Deleted cron job "${name}"`);
}
Dynamic intervals and timeouts
Intervals and timeouts work the same way, but you register the raw timer handle returned by setInterval / setTimeout. The registry tracks them so it can clear them for you on deletion.
addInterval(name: string, milliseconds: number) {
const callback = () => this.logger.log(`Interval "${name}" tick`);
const interval = setInterval(callback, milliseconds);
this.registry.addInterval(name, interval);
}
deleteInterval(name: string) {
this.registry.deleteInterval(name); // clears the timer for you
}
addTimeout(name: string, milliseconds: number) {
const callback = () => this.logger.log(`Timeout "${name}" fired once`);
const timeout = setTimeout(callback, milliseconds);
this.registry.addTimeout(name, timeout);
}
API reference
| Method | Returns | Purpose |
|---|---|---|
addCronJob(name, job) | void | Register a CronJob instance under a name |
getCronJob(name) | CronJob | Retrieve one cron job (throws if missing) |
getCronJobs() | Map<string, CronJob> | All registered cron jobs |
deleteCronJob(name) | void | Stop and remove a cron job |
addInterval(name, ref) | void | Register a setInterval handle |
deleteInterval(name) | void | Clear and remove an interval |
addTimeout(name, ref) | void | Register a setTimeout handle |
deleteTimeout(name) | void | Clear and remove a timeout |
doesExist(type, name) | boolean | Check existence by type ('cron', 'interval', 'timeout') |
Exposing control via a controller
A thin controller turns these methods into an admin API so operators can manage jobs without a redeploy.
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import { JobsService } from './jobs.service';
@Controller('jobs')
export class JobsController {
constructor(private readonly jobs: JobsService) {}
@Get()
list() {
return this.jobs.listCronJobs();
}
@Post()
create(@Body() body: { name: string; cron: string }) {
this.jobs.addCronJob(body.name, body.cron);
return { created: body.name };
}
@Delete(':name')
remove(@Param('name') name: string) {
this.jobs.deleteCronJob(name);
return { deleted: name };
}
}
Best Practices
- Use stable, descriptive job names (e.g.
report:tenant:42) so you can locate and delete them deterministically later. - Guard
addCronJobwithdoesExist('cron', name)to avoid duplicate-name errors when re-registering. - Always pair dynamic creation with deletion in the matching lifecycle hook (
onModuleDestroy, entity removal) to prevent timer leaks. - Persist the schedule definitions you create at runtime, then re-register them in
onApplicationBootstrapso jobs survive restarts. - In multi-instance deployments, dynamic in-memory jobs run on every replica — gate them with a leader lock or move recurring work to a queue.
- Keep job callbacks thin: delegate to an injected service method so the logic stays testable and observable.