npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fluojs/cron

v3.0.2

Published

Decorator-based task scheduling for Fluo with cron, interval, and timeout triggers and optional distributed locking.

Readme

@fluojs/cron

Decorator-based scheduling for fluo applications with lifecycle-managed startup/shutdown and optional Redis distributed locking.

Table of Contents

Installation

npm install @fluojs/cron

@fluojs/cron owns croner as a runtime dependency, so consumers do not need to install the scheduler engine directly.

@fluojs/redis is needed only when Redis distributed locking is enabled. Non-distributed scheduling paths do not load the Redis integration during package import, module registration, bootstrap, or status snapshot creation.

Node.js Compatibility

@fluojs/cron requires Node.js >=24.0.0 <27 as its package-owned support policy. Node.js versions below 24 and Node.js 27+ are not supported. The portable @fluojs/runtime engine omission is a separate contract.

Migration to 3

Before upgrading from @fluojs/cron 2, move deployment hosts to Node.js >=24.0.0 <27. Update any runtime image, CI matrix, or local development environment that still uses a removed Node.js version before installing @fluojs/cron 3.

Migration to 4

Dynamic registry task identity now comes exclusively from the first name argument: remove options.name from addCron, addInterval, and addTimeout calls. Use distributed: { enabled: true } to enable Redis locking and omit distributed to keep it disabled; boolean distributed: true and distributed: false are no longer accepted. Integration code that reads decorator metadata must use getSchedulingTaskMetadata(...), getSchedulingTaskMetadataEntries(...), and schedulingMetadataSymbol; cron-only metadata aliases and metadata writers are no longer public. normalizeCronModuleOptions(...) and NormalizedCronModuleOptions are runtime internals, not application configuration APIs.

When to Use

  • When you need to run periodic background tasks (e.g., database cleanup, report generation).
  • When you want to schedule tasks using standard Cron expressions.
  • When running in a multi-instance environment and you need to ensure a task runs only on one instance at a time (Distributed Locking).
  • When you need simple one-off delayed tasks (Timeout) or fixed-rate intervals.

Quick Start

This scheduling provider/module registration fragment belongs in an existing application graph. Bootstrap through Application Bootstrap Protocol to start tasks through the lifecycle.

Register the CronModule and use decorators to schedule your methods.

Use CronModule.forRoot(...) to register scheduling for an application module. Cron expressions may use either five fields (minute hour day month weekday) or six fields (second minute hour day month weekday). The built-in CronExpression presets use six-field expressions when sub-minute precision is needed. Cron tasks start only after application bootstrap, dynamically registered cron tasks start when added to a started registry, and fluo forwards timezone plus no-overlap protection to the scheduler so one task instance does not overlap itself.

Scheduling decorators apply to public instance methods only. Do not migrate NestJS private scheduled methods, static helpers, or method names that are hidden behind legacy decorator metadata assumptions as-is; expose a public provider/controller method and keep any private implementation details behind that method. Explicit decorator name values must be non-empty strings, matching the dynamic registry validation contract.

import { Module } from '@fluojs/core';
import { CronModule, Cron, CronExpression, Interval, Timeout } from '@fluojs/cron';

class BillingService {
  @Cron(CronExpression.EVERY_MINUTE, { name: 'billing.reconcile' })
  async reconcilePendingInvoices() {
    console.log('Reconciling invoices...');
  }

  @Interval(15_000) // 15 seconds
  async pollStatus() {
    console.log('Polling status...');
  }

  @Timeout(5_000) // 5 seconds after startup
  async initialSync() {
    console.log('Running initial sync...');
  }
}

@Module({
  imports: [CronModule.forRoot()],
  providers: [BillingService],
})
class AppModule {}

Common Patterns

Migrating NestJS Cron Options

NestJS @Cron() options are not a drop-in CronTaskOptions object. Rename NestJS timeZone to fluo timezone:

// NestJS
@Cron('0 9 * * *', { timeZone: 'Asia/Seoul', waitForCompletion: true })

// fluo
@Cron('0 9 * * *', { timezone: 'Asia/Seoul' })

Do not copy waitForCompletion or invent an overlap flag. fluo does not expose either option: every cron task uses scheduler-level no-overlap protection plus an in-process running guard. If another tick arrives while the same task instance is still running, fluo skips that tick instead of queueing another run. A NestJS task with waitForCompletion: true therefore omits the option when migrated. If the NestJS task left waitForCompletion unset or set it to false and intentionally depended on overlapping runs, redesign that work behind an application-owned queue or worker rather than expecting fluo to enable overlap.

This guard covers one task instance in one application process. Use Distributed Locking when multiple application instances must not run the same task concurrently.

The remaining NestJS scheduling options have no CronTaskOptions equivalent. Do not copy utcOffset, unrefTimeout, disabled, threshold, or initialDelay: fluo does not accept or emulate them. Make disabled or category-specific schedules part of application composition by omitting the provider/module that declares them, or register a dynamic task only after the application-owned condition is satisfied. Treat threshold/recovery policy as application-owned work. Use @Timeout(ms, ...) only when a delay relative to application startup is the intended behavior; it is not an absolute-time replacement for NestJS @Cron(Date) or @Cron(DateTime).

| NestJS scheduling form | fluo migration | | --- | --- | | @Cron(expression, { timeZone }) | @Cron(expression, { timezone }) | | @Cron(Date) or @Cron(DateTime) | No direct equivalent. Resolve the absolute-time plan in application code; do not substitute a startup-relative @Timeout. | | @Interval('name', ms) | @Interval(ms, { name: 'name' }) | | @Timeout('name', ms) | @Timeout(ms, { name: 'name' }) | | ScheduleModule.forRootAsync(...) | Resolve async configuration at the application bootstrap/composition boundary, then call synchronous CronModule.forRoot(...). | | NestJS global-by-default schedule registration | CronModule.forRoot({ global: true }) when global visibility is required; fluo defaults to false. | | cronJobs, intervals, or timeouts category switches | No direct equivalent. Own the conditional provider/module composition or explicit dynamic registration in the application. |

Distributed Locking

To prevent scheduled tasks from running concurrently across multiple server instances, enable distributed mode. This requires @fluojs/redis; the Redis peer is loaded and resolved only when distributed.enabled is true.

import { Module } from '@fluojs/core';
import { CronModule } from '@fluojs/cron';
import { RedisModule } from '@fluojs/redis';

@Module({
  imports: [
    RedisModule.forRoot({ host: 'localhost', port: 6379 }),
    CronModule.forRoot({
      distributed: {
        enabled: true,
        keyPrefix: 'fluo:cron:lock',
        lockTtlMs: 30_000,
      },
    }),
  ],
})
class AppModule {}

Leave distributed.clientName unset to keep using the default Redis registration above. To use a non-default Redis connection for distributed locks, set distributed.clientName to the name registered through RedisModule.forRoot({ name, ... }). fluo trims the configured client name during module option normalization and rejects blank values before lifecycle or status reporting uses the Redis dependency name.

distributed.lockTtlMs must stay at or above 1_000ms. When distributed locking is enabled, fluo validates that module-level TTL during option normalization before loading, resolving, or probing Redis. Task-level lockTtlMs overrides are validated only when module distributed mode and that task's distributed locking are both enabled. Disabled module or task locking does not fail solely because an inactive TTL is below the distributed minimum. fluo renews the Redis lock before the active TTL expires, including the minimum supported 1_000ms boundary.

Each scheduler instance uses a platform-neutral default distributed.ownerId; set distributed.ownerId explicitly only when your deployment has a stronger stable-owner convention. When distributed.ownerId is provided, fluo trims it during module option normalization and rejects blank or non-string values before scheduler or Redis lifecycle setup, so invalid or empty owner identifiers cannot enter Redis lock ownership state. Every successful acquisition derives a unique lease token from that owner identity, and renewal and release compare the exact token. A delayed release from an expired acquisition therefore cannot delete a newer lease even when both acquisitions reuse the same configured ownerId. Lock release runs in a finally path after task execution. If bootstrap later fails while a distributed tick is already running, startup rollback keeps the Redis lock client available until that active task can drain and release its lock. If Redis release fails, fluo keeps local ownership in status snapshots and retries during shutdown; if Redis reports that another owner holds the key, local ownership is cleared because fencing has already moved elsewhere. Redis TTL and renewal timing are still drift-sensitive coordination primitives rather than hard fencing tokens, so long-running jobs should remain idempotent and use application-level fencing when stale work would be unsafe.

@Module({
  imports: [
    RedisModule.forRoot({ host: 'localhost', port: 6379 }),
    RedisModule.forRoot({ name: 'locks', host: 'localhost', port: 6380 }),
    CronModule.forRoot({
      distributed: {
        clientName: 'locks',
        enabled: true,
        keyPrefix: 'fluo:cron:lock',
        lockTtlMs: 30_000,
      },
    }),
  ],
})
class MultiRedisCronModule {}

Dynamic Scheduling

You can manage tasks at runtime using the SCHEDULING_REGISTRY.

import { Inject } from '@fluojs/core';
import { SCHEDULING_REGISTRY, type SchedulingRegistry } from '@fluojs/cron';

@Inject(SCHEDULING_REGISTRY)
class TaskManager {
  constructor(private readonly registry: SchedulingRegistry) {}

  addNewTask() {
    this.registry.addCron('dynamic-job', '0 * * * *', () => {
      console.log('Dynamic job running!');
    });
  }

  speedUpPolling() {
    this.registry.updateIntervalMs('inventory.poll', 5_000);
  }

  stopTask() {
    this.registry.remove('dynamic-job');
  }
}

The registry exposes addCron, addInterval, addTimeout, remove, enable, disable, get, getAll, updateCronExpression, and updateIntervalMs. The first name argument is the sole registry key, scheduler metadata name, and default distributed lock identity for dynamic tasks. Dynamic options configure task behavior only; they do not rename a task. Registry and decorator task names must be non-empty strings. get and getAll return immutable SchedulingTaskDescriptor snapshots, not live CronJob handles or mutable registry state. Timeout tasks run once, then disable themselves while remaining in the registry so they can be re-enabled deliberately.

Dynamic cron registration is atomic with scheduler startup: if the scheduler rejects a new cron job, the registry does not retain a half-registered task. Updating a running cron expression or interval cadence is also rollback-safe. A provisional replacement remains tick-ineligible until the previous scheduled handle stops successfully and the registry commits the new handle token; callbacks already queued by a retired handle are ignored. If replacement scheduling fails, fluo restores the previous expression or interval milliseconds and handle before rethrowing the failure. If the previous handle cannot be stopped, fluo stops the provisional replacement and restores the previous state; when that rollback stop also fails, fluo retains the provisional handle for bounded later cleanup without committing its token, so it remains tick-ineligible. Disabling or removing an active task clears its scheduler handle only after stop() succeeds. A stop failure is logged, the handle remains registered for a safe retry, and the operation returns false. A failed disable() leaves the task descriptor disabled so later ticks stay gated while another disable or shutdown retries cleanup; a failed remove() keeps the task registered until a later removal succeeds. If the first shutdown cleanup attempt also fails, the next shutdown lifecycle hook retries each retained handle once and clears it only after stop() succeeds. Cron tasks use both scheduler-level no-overlap protection and fluo's in-process running guard, so the same task instance will not run overlapping ticks.

Bounded Shutdown

CronModule drains active task executions during application shutdown with a bounded timeout so one hung task cannot block process termination forever.

Shutdown closes tick admission before scheduled handles are stopped, so callbacks that were already queued cannot start task bodies or acquire distributed leases after draining begins. By default the shutdown drain waits up to 10_000ms. If that timeout expires, the scheduler logs a warning and continues shutdown without waiting for the hung task to settle. The same shutdown.timeoutMs boundary also applies to Redis owned-lock release I/O during shutdown, so a stuck Redis release cannot block process termination indefinitely. A task's post-run finally release and any immediate stopped-state retry use only the time remaining on the deadline established when shutdown starts, which expires after shutdown.timeoutMs; a task that settles after that deadline does not open a new release window. When distributed locking is enabled, locks held by still-running tasks are not eagerly released on timeout; they remain owned by that task until it settles normally, or until Redis expires the lock after the process exits. fluo calls unref() on lock renewal timers so they continue renewing while other work keeps the Node.js event loop active without retaining the process by themselves, and it clears them when the task settles. If release I/O itself times out, fluo preserves local owned-lock visibility/status and does not clear ownership until Redis confirms release or reports that another owner holds the key. This prevents another node from starting the same job while the original task is still running.

@Module({
  imports: [
    CronModule.forRoot({
      shutdown: {
        timeoutMs: 5_000,
      },
    }),
  ],
})
class AppModule {}

Only singleton providers/controllers are scheduled. Request-scoped and transient scheduled classes are skipped with a warning.

Public API Overview

Modules

  • CronModule.forRoot(options): Configures the scheduler and enables distributed locking if requested.

Decorators

  • @Cron(expression, options?): Schedules a method using a cron expression.
  • @Interval(ms, options?): Schedules a method to run at a fixed interval.
  • @Timeout(ms, options?): Schedules a method to run once after a delay.

Constants & Tokens

  • CronExpression: Enum-like object with common cron patterns, including sub-minute presets such as EVERY_SECOND, EVERY_5_SECONDS, and EVERY_30_SECONDS.
  • SCHEDULING_REGISTRY: Injection token for the SchedulingRegistry service.
  • createCronPlatformStatusSnapshot(...): Creates a status snapshot for health/readiness integrations.
  • Public scheduling types: SchedulingTaskKind, SchedulingTaskCallback, SchedulingTaskOptions, CronTaskOptions, IntervalTaskOptions, TimeoutTaskOptions, DynamicCronTaskOptions, DynamicIntervalTaskOptions, DynamicTimeoutTaskOptions, CronTaskMetadata, IntervalTaskMetadata, TimeoutTaskMetadata, SchedulingTaskMetadata, CronTaskDescriptor, SchedulingTaskDescriptor, and SchedulingRegistry.
  • Public module and scheduler types: CronModuleOptions, CronDistributedOptions, CronShutdownOptions, CronScheduleOptions, CronScheduler, and CronScheduledJob.
  • Public status types: CronLifecycleState, CronStatusAdapterInput, and CronPlatformStatusSnapshot.
  • Metadata integration APIs: getSchedulingTaskMetadata, getSchedulingTaskMetadataEntries, and schedulingMetadataSymbol.

Related Packages

  • @fluojs/redis: Required for distributed locking functionality.
  • @fluojs/core: Required for DI and Module management.
  • croner: The underlying scheduling engine.

Example Sources

  • packages/cron/src/module.test.ts: Comprehensive tests for decorators and module lifecycle.
  • packages/cron/src/service.ts: Runtime scheduling, registry, and shutdown behavior.
  • packages/cron/src/status.test.ts: Status snapshot behavior.
  • packages/cron/src/distributed-lock-manager.ts: Redis distributed lock behavior.