Skip to content
NestJS ns tasks 4 min read

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 addCronJob with a name that already exists throws an error, so guard with registry.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

MethodReturnsPurpose
addCronJob(name, job)voidRegister a CronJob instance under a name
getCronJob(name)CronJobRetrieve one cron job (throws if missing)
getCronJobs()Map<string, CronJob>All registered cron jobs
deleteCronJob(name)voidStop and remove a cron job
addInterval(name, ref)voidRegister a setInterval handle
deleteInterval(name)voidClear and remove an interval
addTimeout(name, ref)voidRegister a setTimeout handle
deleteTimeout(name)voidClear and remove a timeout
doesExist(type, name)booleanCheck 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 addCronJob with doesExist('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 onApplicationBootstrap so 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.
Last updated June 14, 2026
Was this helpful?