Skip to content

Fastify ​

@node-cron/fastify is the official Fastify plugin for node-cron. It registers like @fastify/schedule, the same fastify.register, the same fastify.scheduler decorator, the same start-on-ready / stop-on-close lifecycle, but backed by node-cron, so jobs are described with real cron expressions and you get distributed-ready scheduling, background tasks, timezones and per-execution events.

@fastify/schedule vs @node-cron/fastify ​

@fastify/schedule (toad-scheduler)@node-cron/fastify (node-cron)
register + fastify.scheduler + lifecycleyesyes (same)
Job definitiontoad-scheduler Job objectscron expressions (with seconds)
Run once across a fleetnodistributed: true + a coordinator
Background tasks (forked process)norun: '<file path>'
IANA timezones (DST-correct)notimezone option
Per-execution eventsnotask.on('execution:*')
Overlap control / execution capslimitednoOverlap, maxExecutions

Install ​

fastify (v5) and node-cron are peer dependencies, so the plugin shares the single copy your app already has:

bash
npm install @node-cron/fastify node-cron fastify

Quick start ​

Register the plugin and declare jobs in tasks. They're created on registration and start when Fastify emits onReady, then are destroyed on onClose.

ts
import Fastify from 'fastify';
import { fastifyNodeCron } from '@node-cron/fastify';

const app = Fastify();

await app.register(fastifyNodeCron, {
  tasks: [
    {
      cron: '0 3 * * *', // every day at 03:00
      name: 'nightly-backup',
      run: async () => {
        await runBackup();
      },
    },
  ],
});

await app.listen({ port: 3000 });

Scheduling imperatively ​

The plugin decorates the instance as fastify.scheduler (alias fastify.cron, the same object), so you can schedule from a route or another plugin:

ts
app.post('/reports/enable', async () => {
  app.scheduler.schedule('*/5 * * * *', () => sendReport(), { name: 'report' });
  return { scheduled: true };
});

Plugin options ​

ts
await app.register(fastifyNodeCron, {
  tasks: [ /* FastifyNodeCronTaskDefinition[] */ ],
  runCoordinator,   // RunCoordinator for distributed tasks (process-wide)
  logger,           // custom logger (process-wide); defaults to the Fastify logger
  autoStart: true,  // start tasks on onReady (default true)
});
OptionTypeDescription
tasksFastifyNodeCronTaskDefinition[]Jobs to register at startup. Each is { cron, run, ...options }, where options is any node-cron task option.
runCoordinatorRunCoordinatorCoordinator for distributed: true tasks. See Distributed Coordination.
loggerLoggerCustom logger, applied process-wide.
autoStartbooleanWhen false, nothing starts automatically; drive it via app.scheduler.start() / stop(). Defaults to true.

A task definition is node-cron's options plus cron and run:

ts
{
  cron: '0 3 * * *',          // required: the cron expression
  run: () => doWork(),        // required: an inline function OR a file path (background task)
  name: 'nightly-backup',     // any node-cron option from here on
  timezone: 'America/Sao_Paulo',
  distributed: true,
  noOverlap: true,
  maxExecutions: 10,
}

The scheduler decorator ​

fastify.scheduler exposes a small node-cron-native API:

ts
app.scheduler.schedule(expression, run, options?); // -> ScheduledTask
app.scheduler.getTask(id);                          // by node-cron id
app.scheduler.getTaskByName(name);                  // by your name
app.scheduler.getTasks();                           // Map<id, ScheduledTask>
app.scheduler.start();   // start owned tasks
app.scheduler.stop();    // stop owned tasks (keep them)
app.scheduler.close();   // destroy owned tasks
app.scheduler.cron;      // the underlying node-cron instance

schedule returns a node-cron ScheduledTask, so you get its full API, including lifecycle events:

ts
const task = app.scheduler.schedule('* * * * *', () => work(), { name: 'work' });

task.on('execution:started', () => app.log.info('work started'));
task.on('execution:failed', (ctx) => app.log.error(ctx.execution?.error));

task.getNextRun(); // Date | null

Background tasks ​

Point run at a file path instead of a function and the job runs in a forked child process, so heavy or blocking work never stalls Fastify's event loop. The file must export const task:

ts
// backup.task.js  —  export const task = async (ctx) => { /* heavy work */ };

await app.register(fastifyNodeCron, {
  tasks: [
    { cron: '0 3 * * *', name: 'backup', run: new URL('./backup.task.js', import.meta.url).pathname },
  ],
});

Use an absolute path to the compiled file (__filename in CommonJS, or fileURLToPath(import.meta.url) / new URL(...).pathname in ESM). See Background Tasks for the details.

Distributed scheduling ​

To run a job once per fire across a fleet, pass a run coordinator to the plugin and mark the job distributed: true:

ts
import { RedisLockCoordinator } from '@node-cron/redis-coordinator';

await app.register(fastifyNodeCron, {
  runCoordinator: new RedisLockCoordinator(redis),
  tasks: [
    { cron: '0 3 * * *', name: 'nightly-backup', distributed: true, run: () => runBackup() },
  ],
});

The plugin makes a task coordination-ready; the per-fire, fleet-wide election is the coordinator's job. Without one, distributed tasks fall back to node-cron's NODE_CRON_RUN env-var default (a single designated runner). The full model is in Distributed Coordination.

Migrating from @fastify/schedule ​

Registration, the decorator name, and the lifecycle are the same. What changes is how you describe a job: a cron expression instead of a toad-scheduler Job.

ts
// Before — @fastify/schedule + toad-scheduler
import { fastifySchedule } from '@fastify/schedule';
import { AsyncTask, SimpleIntervalJob } from 'toad-scheduler';

await app.register(fastifySchedule);
const task = new AsyncTask('poll', () => pollForData());
app.scheduler.addSimpleIntervalJob(new SimpleIntervalJob({ minutes: 5 }, task));
ts
// After — @node-cron/fastify
import { fastifyNodeCron } from '@node-cron/fastify';

await app.register(fastifyNodeCron);
app.scheduler.schedule('*/5 * * * *', () => pollForData(), { name: 'poll' });

Interval jobs map to cron expressions. For sub-minute work, node-cron's 6-field expression has seconds, e.g. '*/20 * * * * *' for every 20 seconds.

Next steps ​

Released in 2016 under the ISC License.