--- url: /getting-started.md description: >- Install node-cron and run your first scheduled task in five minutes, in CommonJS and ESM, plus what actually happens when a task runs. --- # Quickstart `node-cron` is a lightweight task scheduler for Node.js, written in TypeScript and inspired by [GNU crontab](https://www.gnu.org/software/mcron/manual/html_node/Crontab-file.html). It runs recurring tasks on a schedule you describe with standard cron syntax, and has **zero runtime dependencies**. This page gets you from zero to a running task in five minutes. By the end you'll have scheduled a task, seen it run, and know where to go next. ## 1. Install ```bash npm install node-cron ``` `node-cron` ships both CommonJS and ESM builds and bundled TypeScript types, so it works out of the box in any modern Node.js project. > Requires **Node.js 20 or newer**. Tested on Node 20, 22, and 24. ## 2. Schedule your first task Import node-cron and schedule a function to run every minute. ::: code-group ```js [ESM] import cron from 'node-cron'; cron.schedule('* * * * *', () => { console.log('Running a task every minute'); }); ``` ```js [CommonJS] const cron = require('node-cron'); cron.schedule('* * * * *', () => { console.log('Running a task every minute'); }); ``` ::: That's it. The `* * * * *` expression means "every minute". Run the file and you'll see the message print at the top of each minute. ## 3. What just happened `cron.schedule(expression, task)` does two things: 1. **Creates** a scheduled task from your cron expression and function. 2. **Starts** it immediately, so it begins matching the clock right away. It returns a [`ScheduledTask`](/task-lifecycle) object you can hold onto to control the task later: ```js import cron from 'node-cron'; const task = cron.schedule('* * * * *', () => { console.log('tick'); }); task.stop(); // pause it task.start(); // resume it task.destroy(); // remove it for good ``` > πŸ’‘ Want a task that does **not** start right away? Use [`cron.createTask`](/api-reference#createtask-expression-func-options) instead of `cron.schedule`. Same arguments, but you call `.start()` yourself. ## 4. Tasks receive a context Every task function is called with a `TaskContext` describing the run, useful for logging and metrics: ```js import cron from 'node-cron'; cron.schedule('* * * * *', (ctx) => { console.log(`scheduled for: ${ctx.dateLocalIso}`); console.log(`started at: ${ctx.triggeredAt.toISOString()}`); }); ``` The full `TaskContext` shape, and the events that carry it, is covered in [Events & Observability](/event-listening#taskcontext). ## Next steps You've scheduled, run, and controlled a task. Now learn to express *exactly* when it should run: * **[Cron Syntax](/cron-syntax)**: ranges, steps, lists, and named months/weekdays. * [Task Lifecycle & Status](/task-lifecycle): what `start`, `stop`, and `getStatus` actually do. * [Scheduling Options](/scheduling-options): timezones, overlap prevention, and execution limits. --- --- url: /usage-rules.md description: >- When to use node-cron and when not to. A decision matrix for schedule vs createTask vs background tasks, plus node-cron's limits as an in-process, non-durable, non-distributed scheduler. --- # Usage Rules & Limits A quick guide to picking the right tool and the right API. node-cron is an **in-process** scheduler: it runs tasks inside your Node.js process (or a forked child process for background tasks). It is small, dependency-free, and great for recurring work that lives with your app. It is **not** a durable or distributed job queue. ## Decision matrix | Goal | Use | | ------------------------------------------------- | ------------------------------------------------------------------- | | Run a function on a schedule, starting now | [`cron.schedule(expr, fn)`](/api-reference#schedule-expression-func-options) | | Configure or attach listeners before it starts | [`cron.createTask(...)`](/api-reference#createtask-expression-func-options) then `.start()` | | Prevent overlapping runs | [`noOverlap: true`](/scheduling-options) | | Run CPU-heavy, blocking, or long work | [Background task](/background-tasks) (pass a file path) | | React to success, failure, or missed runs | [`task.on('execution:*')`](/event-listening) | | Run on the last day of the month | [`L` in the day-of-month field](/cron-syntax#last-day-of-the-month-l) | | Run a fixed number of times | [`maxExecutions`](/scheduling-options) | | Stagger many tasks firing at once | [`maxRandomDelay`](/scheduling-options) | | Route internal logs through your logger | [`setLogger` / `logger`](/logging) | | Durable, distributed, retried jobs | **Not node-cron.** Use [Sidequest](https://sidequestjs.com) or a queue | ## Usage rules * Use **`cron.schedule`** when you want the task to start immediately. * Use **`cron.createTask`** when you need to attach [event listeners](/event-listening) or inspect the task before it starts, then call `.start()`. * Use a **[background task](/background-tasks)** (a file path instead of a function) for CPU-bound, blocking, or long-running work, so it does not block the main event loop. * Use **`noOverlap: true`** when a run can take longer than its interval and you do not want overlapping executions. * Attach **`execution:failed`** / **`execution:missed`** listeners to observe problems; listening to `execution:missed` also silences the default missed-execution warning. * Set a **`timezone`** so schedules are unambiguous across environments. ## Limits: when not to use node-cron node-cron deliberately stays small. It does **not** provide: * **Durability.** Schedules live in memory. If the process restarts, pending runs are lost and node-cron does not "catch up" runs it missed while down. * **Distribution or coordination.** Each process runs its own schedule independently. There is no leader election or locking, so if you run **N replicas**, a task scheduled in each one runs **N times**. Guard cluster jobs yourself (run the scheduler on a single instance, or add your own lock). * **Persistent retries, uniqueness, priorities, or a dashboard.** If you need any of the above (durable jobs that survive restarts, retries with backoff, exactly-once across a cluster, a monitoring UI), reach for a durable job runner such as [**Sidequest.js**](https://sidequestjs.com) or a queue backed by a datastore, rather than node-cron alone. ## Next steps * [Quickstart](/getting-started): install and run your first task. * [Cookbook](/cookbook): copy-paste recipes for the cases above. --- --- url: /cron-syntax.md description: >- How node-cron interprets cron expressions, covering fields, allowed values, ranges, steps, lists, named months and weekdays, plus a copy-paste table of common schedules. --- # Cron Syntax A cron expression is how you tell node-cron *when* a task should run. `node-cron` uses the standard cron format with **five or six fields**, where the leading **seconds** field is optional. ```plaintext # β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ second (optional) # β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ minute # β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ hour # β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€ day of month # β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€ month # β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€ day of week # β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ # * * * * * * ``` When you provide **five** fields, the seconds field defaults to `0` (the task runs at the start of the matched minute). Provide **six** fields to schedule down to the second. ::: tip Prefer plain English? [`cron-translate`](/cron-translate) turns phrases like `every weekday at 6pm` into cron expressions (and back), so you can write `cron.schedule(toCron('every weekday at 6pm'), ...)` without hand-counting fields. ::: ## Allowed values per field | Field | Values | | -------------- | ----------------------------------- | | second | `0-59` (optional) | | minute | `0-59` | | hour | `0-23` | | day of month | `1-31` (or [`L`](#last-day-of-the-month-l), [`L-n`](#n-days-before-the-last-day-l-n), [`15W`/`LW`](#nearest-weekday-w), [`?`](#no-specific-value)) | | month | `1-12` (or names, e.g. `Jan`, `Sep`)| | day of week | `0-7` (or names; `0` and `7` are Sunday; [`2#3`](#nth-weekday), [`5L`](#last-weekday-of-the-month-weekdayl), [`?`](#no-specific-value)) | Each field also accepts `*` (any value), ranges, steps, and comma-separated lists, described below. The whole expression can also be a [nickname](#nicknames-daily-hourly) like `@daily`. ## Common schedules Copy-paste reference for the expressions you'll reach for most often: | Expression | Runs | | ---------------- | ------------------------------------- | | `* * * * *` | Every minute | | `*/5 * * * *` | Every 5 minutes | | `0 * * * *` | Every hour, on the hour | | `0 0 * * *` | Every day at midnight | | `0 3 * * *` | Every day at 03:00 | | `0 9 * * 1-5` | At 09:00, Monday through Friday | | `0 0 1 * *` | At midnight on the 1st of each month | | `0 0 L * *` | At midnight on the last day of each month | | `0 0 * * 0` | At midnight every Sunday | | `0 0 12 * * 1#1` | At 12:00 on the first Monday of every month | | `0 0 18 * * 5L` | At 18:00 on the last Friday of every month | | `0 0 15W * *` | At midnight on the weekday nearest the 15th | | `0 0 L-3 * *` | At midnight 3 days before the end of the month | | `*/30 * * * * *` | Every 30 seconds (6-field form) | | `@daily` | Every day at midnight (nickname) | ## Building expressions ### Lists: multiple specific values Use commas to run a task at several specific values in a field. ```js import cron from 'node-cron'; // Runs at minutes 1, 2, 4, and 5 of every hour cron.schedule('1,2,4,5 * * * *', () => { console.log('Running at minutes 1, 2, 4, and 5 of each hour'); }); ``` > This expression has five fields, so the **seconds** field is omitted and defaults to `0`. ### Ranges: a continuous interval Use a dash (`-`) to define an inclusive range. ```js import cron from 'node-cron'; // Runs every minute from minute 1 to minute 5 (inclusive) of every hour cron.schedule('1-5 * * * *', () => { console.log('Running every minute from 1 to 5'); }); ``` This is equivalent to `1,2,3,4,5`, a cleaner way to express consecutive values. ### Steps: periodic intervals Use a slash (`/`) after a wildcard or range to define a step. ```js import cron from 'node-cron'; // Every 2 minutes (even minutes: 0, 2, 4, ...) cron.schedule('*/2 * * * *', () => { console.log('Running every 2 minutes (even minutes)'); }); // Every 2 minutes starting from 1 (odd minutes: 1, 3, 5, ...) cron.schedule('1-59/2 * * * *', () => { console.log('Running every 2 minutes starting from 1 (odd minutes)'); }); ``` * `*/2` means "every 2 units" across the field's full range (`0-59` for minutes), covering all even minutes. * `1-59/2` means "every 2 units starting from 1", covering all odd minutes. ### Names: months and weekdays For readability, use full or abbreviated names instead of numbers for months and days of the week. ```js import cron from 'node-cron'; // Every minute on Sundays in January and September cron.schedule('* * * January,September Sunday', () => { console.log('Running on Sundays in January and September'); }); // Same schedule using short names cron.schedule('* * * Jan,Sep Sun', () => { console.log('Running on Sundays in Jan and Sep'); }); ``` ### Last day of the month: `L` In the **day of month** field, `L` (or lowercase `l`) means the last calendar day of the month. node-cron resolves it per month, so it lands on the 28th, 29th, 30th, or 31st as appropriate, including leap years. ```js import cron from 'node-cron'; // Runs at 12:00 on the last day of every month cron.schedule('0 0 12 L * *', () => { console.log('Running on the last day of the month'); }); ``` You can combine `L` with explicit days in a list: ```js import cron from 'node-cron'; // Runs at midnight on the 15th and on the last day of every month cron.schedule('0 0 15,L * *', () => { console.log('Running on the 15th and the last day'); }); ``` > Bare `L` is valid **only** in the day of month field. In the day of week field, use the `L` form described below. ### `n` days before the last day: `L-n` {#n-days-before-the-last-day-l-n} In the **day of month** field, `L-n` means the day `n` days *before* the last day of the month. `L-1` is the second-to-last day, `L-3` the fourth-to-last, and so on. node-cron resolves it per month, so it tracks the varying month length. ```js import cron from 'node-cron'; // Runs at midnight 3 days before the end of every month cron.schedule('0 0 L-3 * *', () => { console.log('Three days before month end'); }); ``` > If the offset reaches past the start of a month (e.g. `L-29` in February), no day matches that month. ### Nearest weekday: `W` {#nearest-weekday-w} In the **day of month** field, `nW` matches the nearest weekday (Monday to Friday) to day `n`, without crossing into an adjacent month: a Saturday shifts back to Friday, a Sunday forward to Monday, and the adjustment reverses at a month boundary rather than leaving the month. Use `LW` for the last weekday of the month. Weekends are the only adjustment; there is no holiday awareness. ```js import cron from 'node-cron'; // Runs at midnight on the weekday nearest the 15th cron.schedule('0 0 15W * *', () => { console.log('Nearest weekday to the 15th'); }); // Runs at midnight on the last weekday of every month cron.schedule('0 0 LW * *', () => { console.log('Last weekday of the month'); }); ``` > `W` suffixes a single day number or `L`. It cannot be used in a range or step (`1-15W`, `15W/2` are rejected). ### Nth weekday: `#` {#nth-weekday} In the **day of week** field, `#` matches the nth occurrence of that weekday in the month. The weekday is `0-7` (Sunday through Saturday, where `0` and `7` are both Sunday), and the occurrence is `1-5`. ```js import cron from 'node-cron'; // Runs at 12:00 on the first Monday of every month cron.schedule('0 12 * * 1#1', () => { console.log('First Monday of the month'); }); // Runs at 09:00 on the 3rd Tuesday of every month cron.schedule('0 9 * * 2#3', () => { console.log('Third Tuesday of the month'); }); ``` If the nth occurrence does not exist in a given month (e.g. `0#5` in a month with only four Sundays), that month is simply skipped. ### Last weekday of the month: `L` {#last-weekday-of-the-month-weekdayl} In the **day of week** field, `L` matches the last occurrence of that weekday in the month. ```js import cron from 'node-cron'; // Runs at 18:00 on the last Friday of every month cron.schedule('0 18 * * 5L', () => { console.log('Last Friday of the month'); }); // Runs at midnight on the last Sunday of every month cron.schedule('0 0 * * 0L', () => { console.log('Last Sunday of the month'); }); ``` > Invalid forms like `8#1`, `2#6`, `2#0`, `8L`, `L5`, or bare `L` in the weekday field are rejected by [`validate`](#validating-expressions). ### No specific value: `?` {#no-specific-value} `?` is a Quartz-style alias for `*` (any value), accepted **only** in the day-of-month and day-of-week fields. It reads as "no specific value here", a convention some schedulers require when you constrain one of the two day fields and want to leave the other unconstrained. In node-cron it behaves exactly like `*`. ```js import cron from 'node-cron'; // Every day at 12:00, day-of-week left unconstrained cron.schedule('0 0 12 * * ?', () => { console.log('Noon, any weekday'); }); ``` > `?` is rejected outside the two day fields, and cannot appear in a list (`1,?`). ## Nicknames: `@daily`, `@hourly`, … {#nicknames-daily-hourly} Instead of a full expression you can pass a nickname. node-cron expands it to the equivalent expression before scheduling, so [`validate`](#validating-expressions) accepts them too. | Nickname | Equivalent | Runs | | ------------------------ | ------------- | ---------------------------------- | | `@yearly` / `@annually` | `0 0 1 1 *` | Once a year, at midnight on Jan 1 | | `@monthly` | `0 0 1 * *` | Once a month, at midnight on the 1st | | `@weekly` | `0 0 * * 0` | Once a week, at midnight on Sunday | | `@daily` / `@midnight` | `0 0 * * *` | Once a day, at midnight | | `@hourly` | `0 * * * *` | Once an hour, on the hour | ```js import cron from 'node-cron'; cron.schedule('@daily', () => { console.log('Runs every day at midnight'); }); ``` > Nicknames are case-insensitive. There is no `@reboot` nickname, node-cron has no concept of process start as a schedule; use `task.execute()` for an immediate run. ## Validating expressions Not sure an expression is valid? Check it before scheduling: ```js import cron from 'node-cron'; cron.validate('0 12 * * *'); // true cron.validate('not a cron'); // false ``` For tooling and richer error messages, two more helpers go beyond a boolean: * [`validateDetailed(expr)`](/api-reference#validatedetailed-expression) returns **every** problem (which field, value, and why) without throwing. * [`parse(expr)`](/api-reference#parse-expression) returns the decomposed fields, or throws on the first invalid one. ```js import { validateDetailed } from 'node-cron'; validateDetailed('99 12 * * 9').errors; // [ { field: 'minute', value: '99', ... }, { field: 'dayOfWeek', value: '9', ... } ] ``` See [`validate`](/api-reference#validate-expression), [`validateDetailed`](/api-reference#validatedetailed-expression), and [`parse`](/api-reference#parse-expression) in the API reference. ## Next steps Now that you can describe *when* a task runs, learn how to manage it once it's running: * **[Task Lifecycle & Status](/task-lifecycle)**: start, stop, inspect, and destroy tasks. * [Scheduling Options](/scheduling-options): timezones, overlap prevention, and limits. --- --- url: /task-lifecycle.md description: >- How a node-cron task moves through stopped, idle, running, and destroyed, and how to control it with start, stop, destroy, execute, getStatus, and getNextRun. --- # Task Lifecycle & Status Both `cron.schedule` and `cron.createTask` return a `ScheduledTask`: a single, consistent interface for controlling and inspecting a task, whether it runs inline (in your process) or as a [background task](/background-tasks) (in a forked process). This page explains the states a task moves through and the methods you use to drive it. ## The lifecycle states Every task is always in exactly one of four states: | Status | Meaning | | ------------- | -------------------------------------------------------------- | | `stopped` | Scheduler is not running. The task will not fire. | | `idle` | Scheduler is running and waiting for the next match. | | `running` | The task function is currently executing. | | `destroyed` | The task is permanently removed and cannot be restarted. | They transition like this: ``` start() (match fires) stopped ──────────▢ idle ──────────────────▢ running β–² β”‚ β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ (execution ends) β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ stop() any state ──── destroy() ────▢ destroyed (terminal) ``` * `cron.schedule(...)` returns a task already in **`idle`** (it auto-starts). * `cron.createTask(...)` returns a task in **`stopped`** until you call `.start()`. * Once **`destroyed`**, a task is gone; only `getStatus()` remains meaningful. Read the current state at any time with [`getStatus()`](#getstatus). ## Inline vs. background: sync vs. async The control methods share one interface but differ in return type: * **Inline tasks** (a function) act **synchronously**: `start()`, `stop()`, and `destroy()` return `void`. * **Background tasks** (a file path) cross a process boundary, so the same methods return a **`Promise`** you should `await`. The interface reflects this with `void | Promise`, so writing `await task.stop()` is safe for both; it just resolves immediately for inline tasks. ```js import cron from 'node-cron'; // Inline: synchronous const task = cron.schedule('* * * * *', () => {}); task.getStatus(); // 'idle' task.stop(); task.getStatus(); // 'stopped' // Background: asynchronous const bg = cron.schedule('* * * * *', './tasks/job.js'); await bg.stop(); // wait for the forked process to terminate bg.getStatus(); // 'stopped' ``` ## The ScheduledTask interface ```ts export interface ScheduledTask { id: string; name?: string; start(): void | Promise; stop(): void | Promise; destroy(): void | Promise; execute(): Promise; getStatus(): string; getNextRun(): Date | null; // Inspection getNextRuns(count: number): Date[]; match(date: Date): boolean; msToNext(): number | null; isBusy(): boolean; runsLeft(): number | undefined; getPattern(): string; lastRun(): LastRun | null; unref(): void; ref(): void; on(event: TaskEvent, fn: (context: TaskContext) => void | Promise): void; off(event: TaskEvent, fn: (context: TaskContext) => void | Promise): void; once(event: TaskEvent, fn: (context: TaskContext) => void | Promise): void; } ``` The `on`, `off`, and `once` methods are covered in [Events & Observability](/event-listening). ## Methods ### `start()` Starts the scheduler, moving the task from `stopped` to `idle`. * **Inline tasks:** begins evaluating the cron expression and runs the function at matched times. * **Background tasks:** forks a dedicated process and starts a daemon that handles scheduling. * Has no effect if the task is already running. ### `stop()` Stops the scheduler and prevents future runs, moving the task to `stopped`. * **Inline tasks:** halts scheduling, but a run already in progress is allowed to finish. * **Background tasks:** terminates the child process. * This does **not** remove the task; use [`destroy()`](#destroy) for that. A stopped task can be started again. ### `destroy()` Permanently deactivates the task and releases its resources, moving it to `destroyed`. * **Background tasks:** kills the associated process and detaches listeners. * After destruction, don't call any method other than `getStatus()`. * The task is also removed from the module registry (so it no longer appears in [`getTasks()`](/api-reference#gettasks)). ### `execute()` Runs the task function **immediately**, outside its schedule. Always returns a `Promise`. ```js import cron from 'node-cron'; const task = cron.schedule('0 3 * * *', async () => { return doBackup(); }); // Trigger a run right now without waiting for 03:00 const result = await task.execute(); ``` * Useful for testing, debugging, or ad-hoc runs. * Resolves with the task's return value, or rejects if it throws. * Emits the same lifecycle events as a scheduled run, with `execution.reason === 'invoked'`. ### `getStatus()` Returns the current state as a string: `'stopped'`, `'idle'`, `'running'`, or `'destroyed'`. Synchronous for both task types. ```js import cron from 'node-cron'; const task = cron.schedule('* * * * *', () => {}); console.log(task.getStatus()); // 'idle' ``` ### `getNextRun()` Returns the next scheduled run time as a `Date`, or `null` if the task is stopped, destroyed, or its expression yields no future match. ```js import cron from 'node-cron'; const task = cron.schedule('0 * * * *', () => {}); console.log(task.getNextRun()); // e.g. 2026-06-16T15:00:00.000Z ``` ## Inspecting a task Beyond status and the next run, a task exposes a few read-only methods for previewing its schedule and seeing what it's doing right now, handy for dashboards, health checks, and tests. They work the same for inline and background tasks. ### `getNextRuns(count)` Returns the next `count` run times as an array of `Date`s, strictly increasing. Useful for previewing a schedule ("when will this fire next?") without waiting. ```js import cron from 'node-cron'; const task = cron.schedule('0 0 12 * * *', () => {}); task.getNextRuns(3); // [ 2026-06-18T12:00:00.000Z, 2026-06-19T12:00:00.000Z, 2026-06-20T12:00:00.000Z ] ``` A non-positive `count` returns an empty array. Unlike [`getNextRun()`](#getnextrun), this works regardless of the task's state (it computes from the expression). ### `match(date)` Returns `true` if the given `Date` matches the task's cron expression (evaluated in the task's timezone). ```js const task = cron.schedule('0 0 12 * * *', () => {}, { timezone: 'Etc/UTC' }); task.match(new Date('2026-06-18T12:00:00Z')); // true task.match(new Date('2026-06-18T12:00:01Z')); // false ``` ### `msToNext()` Milliseconds from now until the next run, or `null` when the task is stopped. ```js const task = cron.schedule('0 * * * *', () => {}); task.msToNext(); // e.g. 1830000 ``` ### `isBusy()` `true` while an execution is in progress (the task is in the `running` state), `false` otherwise. Useful before triggering work or shutting down. ```js if (!task.isBusy()) await task.execute(); ``` ### `runsLeft()` When [`maxExecutions`](/scheduling-options) is set, the number of runs remaining before the task destroys itself; otherwise `undefined`. ```js const task = cron.schedule('* * * * * *', () => {}, { maxExecutions: 3 }); task.runsLeft(); // 3, then 2, 1, 0 ``` ### `getPattern()` Returns the original cron expression the task was created with. ```js const task = cron.schedule('0 0 12 * * *', () => {}); task.getPattern(); // '0 0 12 * * *' ``` ### `lastRun()` Returns information about the last actual execution, or `null` if the task has not run yet. ```js const task = cron.schedule('* * * * *', async () => { return fetchData(); }); // Before any execution task.lastRun(); // null // After a successful run task.lastRun(); // { date: Date, result: ... } // After a failed run task.lastRun(); // { date: Date, error: Error } ``` The `date` reflects when the execution actually ran (finish time), not when the scheduler tick was checked. The return type is: ```ts type LastRun = { date: Date; result?: unknown; error?: Error }; ``` `LastRun` is exported from the package for TypeScript users. ## Keeping the process alive: `unref()` / `ref()` {#unref-ref} By default a running task keeps an active timer, which keeps the Node.js process alive. In a long-running server that's what you want. In a short-lived CLI or script, it can stop the process from exiting once the real work is done. `task.unref()` [unref](https://nodejs.org/api/timers.html#timeoutunref)'s the task's internal timer, so the task keeps firing as long as *something else* keeps the process up, but no longer holds it open on its own. `task.ref()` reverses it. ```js import cron from 'node-cron'; const task = cron.schedule('* * * * * *', () => heartbeat()); task.unref(); // this task alone won't keep the process running ``` To start a task in the unref'd state without a separate call, pass the [`unref`](/scheduling-options) option to `schedule`/`createTask`. > Applies to inline tasks. Background tasks run in a forked process and don't hold the parent's event loop open through this timer. ## Creating a stopped task When you need a task that doesn't start immediately (to attach listeners first, start it conditionally, or control timing in tests), use `cron.createTask` and start it yourself: ```js import cron from 'node-cron'; const task = cron.createTask('* * * * *', () => { console.log('manually started'); }); task.getStatus(); // 'stopped' task.start(); task.getStatus(); // 'idle' ``` ## Next steps * **[Scheduling Options](/scheduling-options)**: fine-tune behavior with timezones, overlap prevention, and limits. * [Events & Observability](/event-listening): react to the lifecycle transitions described here. --- --- url: /scheduling-options.md description: >- Fine-tune node-cron tasks with timezone, noOverlap, maxExecutions, maxRandomDelay, name, logger, and suppressMissedWarning, with examples for each. --- # Scheduling Options A bare `cron.schedule(expression, task)` already covers the common case. As your jobs grow more demanding, the optional third argument lets you tune *how* they run, without changing the rest of your code. ```js cron.schedule(expression, task, options); ``` ## All options ```ts export type Options = { name?: string; timezone?: string; noOverlap?: boolean; maxExecutions?: number; maxRandomDelay?: number; logger?: Logger; suppressMissedWarning?: boolean; missedExecutionTolerance?: number; distributed?: boolean; // run on one instance per fire across a fleet runCoordinator?: RunCoordinator; // per-task coordinator (overrides the global one) distributedLease?: number; // lease ms for lease-based coordinators (default 30000) unref?: boolean; // don't keep the process alive for this task's timer executeTimeout?: number; // background tasks only startTimeout?: number; // background tasks only }; ``` | Option | Type | Default | Description | | ----------------------- | --------- | ------- | --------------------------------------------------------------------------- | | `name` | `string` | task id | A human-readable identifier for the task. Useful for logging, debugging, and dashboards. | | `timezone` | `string` | system | The timezone the cron expression is evaluated in. Any IANA name recognized by `Intl.DateTimeFormat` (e.g. `"America/Sao_Paulo"`, `"UTC"`, `"Europe/London"`). See [Timezones & DST](/timezones-and-dst) for behavior across daylight-saving transitions. | | `noOverlap` | `boolean` | `false` | If `true`, a scheduled run is **skipped** when the previous run is still executing, preventing overlapping executions. | | `maxExecutions` | `number` | none | Maximum number of times the task may run. After the limit, the task is automatically destroyed. | | `maxRandomDelay` | `number` | `0` | Adds up to this many milliseconds of random delay (jitter) before each run. Spreads out tasks that would otherwise fire simultaneously. | | `logger` | `Logger` | global | A custom [logger](/logging) for this task, overriding the global one. **Not supported for [background tasks](/background-tasks).** | | `suppressMissedWarning` | `boolean` | `false` | Silences the "missed execution" warning for this task. See [Logging](/logging#suppressing-the-missed-execution-warning). | | `missedExecutionTolerance` | `number` | `1000` | How late (in ms) a scheduled run may wake and still execute instead of being reported as missed. Long timers drift (OS sleep, GC, throttling, clock skew), which can otherwise skip daily/weekly runs. Always capped to the gap to the next run, so it can never run a slot twice. | | `distributed` | `boolean` | `false` | Run this task on a **single instance per fire** across a fleet. Requires a `name`. Uses the `NODE_CRON_RUN` env-var default (one designated runner) unless a coordinator is set. See [Distributed Coordination](/distributed-coordination). | | `runCoordinator` | `RunCoordinator` | global | A per-task [run coordinator](/distributed-coordination#high-availability-a-custom-run-coordinator), overriding the one set with `setRunCoordinator`. Only used when `distributed` is `true`. | | `distributedLease` | `number` | `30000` | Lease expiry (ms) passed to lease-based coordinators (e.g. a Redis lock) in case the holder crashes mid-run. Must exceed the run time (and, when combined with `maxRandomDelay`, the jitter too, the lease is taken before the jitter). See [Distributed Coordination](/distributed-coordination#distributedlease). Ignored by the env-var default. | | `unref` | `boolean` | `false` | If `true`, the task's internal timer is [`unref`](https://nodejs.org/api/timers.html#timeoutunref)'d so it won't keep the Node.js process alive on its own. Handy for CLI tools and scripts that should exit once their real work is done. Toggle at runtime with [`task.unref()` / `task.ref()`](/task-lifecycle#unref-ref). | > πŸ›ˆ There is no `scheduled` or `runOnInit` option anymore. Tasks created with `cron.schedule` start immediately; for a task that starts stopped, use [`cron.createTask`](/task-lifecycle#creating-a-stopped-task). To run a task immediately on demand, call [`task.execute()`](/task-lifecycle#execute). See [Migrating from v3](/migrating-from-v3). ## Examples ### Default options ```js import cron from 'node-cron'; const task = cron.schedule('* * * * *', () => { console.log('Running every minute'); }); ``` Runs immediately on creation, in the system timezone, with overlapping runs allowed and no execution limit. ### Custom timezone Evaluate the expression in a specific timezone, regardless of where the server runs: ```js import cron from 'node-cron'; const task = cron.schedule('0 0 * * *', () => { console.log('Midnight in SΓ£o Paulo'); }, { timezone: 'America/Sao_Paulo', }); ``` ### Prevent overlapping runs If a task can take longer than its interval, `noOverlap` skips a run rather than starting it on top of the previous one: ```js import cron from 'node-cron'; const task = cron.schedule('* * * * *', async () => { await slowJob(); // may take longer than a minute }, { noOverlap: true, }); ``` A skipped run emits an [`execution:overlap`](/event-listening) event. ### Limit the number of runs The task destroys itself after the limit is reached: ```js import cron from 'node-cron'; const task = cron.schedule('* * * * *', () => { console.log('This runs 5 times, then the task is destroyed'); }, { maxExecutions: 5, }); ``` Reaching the limit emits [`execution:maxReached`](/event-listening). For a one-shot task, set `maxExecutions: 1`. ### Add jitter to avoid a thundering herd When many instances schedule the same job at the same instant (e.g. across a fleet), `maxRandomDelay` staggers them: ```js import cron from 'node-cron'; const task = cron.schedule('0 * * * *', () => { refreshCache(); }, { maxRandomDelay: 30_000, // up to 30s of random delay per run }); ``` ### Run on one instance across a fleet When several copies of your app run the same schedule, `distributed` makes a task fire on only one instance per scheduled time. Out of the box you designate the runner with the `NODE_CRON_RUN` env var; for high availability, register a [run coordinator](/distributed-coordination#high-availability-a-custom-run-coordinator) (e.g. Redis): ```js import cron from 'node-cron'; cron.schedule('0 3 * * *', runNightlyBackup, { name: 'nightly-backup', // required: forms the coordination key distributed: true, }); ``` A not-elected instance emits [`execution:skipped`](/event-listening) instead of running. See [Distributed Coordination](/distributed-coordination) for the full picture. ### Name a task A `name` makes logs and dashboards readable, and is exposed as `task.name`: ```js import cron from 'node-cron'; const task = cron.schedule('0 3 * * *', () => {}, { name: 'nightly-backup', }); console.log(task.name); // 'nightly-backup' ``` ### Tolerate late executions A heartbeat is armed to fire at the scheduled time, but long timers drift (OS sleep, GC, CPU throttling, clock skew), so the callback can wake a little late. By default a run that wakes within `missedExecutionTolerance` (1000ms) still executes; later than that, it is reported as [missed](/event-listening). On an underpowered host, or a daily/weekly job that can wake several seconds late, raise it: ```js import cron from 'node-cron'; const task = cron.schedule('0 3 * * 0', runWeeklyBackup, { missedExecutionTolerance: 5 * 60_000, // still run if we wake up to 5 min late }); ``` The tolerance is always capped to the gap until the next run, so it can never run the same slot twice. A late run fires once and the next slot is scheduled normally. > πŸ›ˆ **Background tasks** accept two extra options, `executeTimeout` and `startTimeout`, covered in [Background Tasks](/background-tasks#manual-execution-and-executetimeout). ## Next steps * **[Events & Observability](/event-listening)**: hook into overlap, missed runs, failures, and more. * [Logging](/logging): route node-cron's output through your own logger. --- --- url: /timezones-and-dst.md description: >- How node-cron evaluates schedules in a timezone, exactly what it does across daylight-saving transitions (spring-forward gaps and fall-back overlaps), and how to opt out of DST entirely. --- # Timezones & DST A cron expression describes a **wall-clock** time. `30 2 * * *` means "02:30 on the clock of the task's timezone", not a fixed UTC instant. The hard part is daylight-saving time (DST), when the local clock jumps forward or back. This page explains exactly what node-cron does, so you can predict every run. By default a task runs in the **system timezone**. Set [`timezone`](/scheduling-options) to pin it to a specific zone: ```js cron.schedule('30 2 * * *', task, { timezone: 'America/New_York' }); ``` ## The guarantees Whatever the timezone and whatever DST does, node-cron holds these invariants: 1. **Wall-clock first.** The expression is matched against the local time of the task's timezone. 2. **Never in the past.** The next run is always strictly after the current instant. 3. **Never twice for the same instant.** Even when the local clock repeats an hour, each absolute instant fires at most once. 4. **Always moves forward.** Successive runs are strictly increasing in absolute time, never going backwards when the local clock does. ## Spring-forward (the gap) When the clock springs forward (e.g. `02:00 β†’ 03:00`), the times inside the gap **don't exist**. node-cron **skips** them rather than guessing an adjacent time. * A daily time that lands in the gap is skipped **for that day**. `30 2 * * *` in `America/New_York` has no `02:30` on the spring-forward day, so that day is skipped and the next run is the following day at `02:30`. * A sub-daily expression resumes at the first valid time after the gap. `*/15 * * * *` goes `… 01:45, 03:00, 03:15 …` β€” `02:00`–`02:45` never fire. The reasoning: you configured `02:30` on purpose. Firing at `03:00` instead would be surprising; skipping is predictable. ## Fall-back (the overlap) When the clock falls back (e.g. `02:00 β†’ 01:00`), the times inside the overlap exist **twice**. node-cron fires on the **first** occurrence and ignores the second. * `30 1 * * *` in `America/New_York` fires once, at `01:30` in the pre-transition offset (EDT). The second `01:30` (EST) is ignored that day. * Sub-hourly expressions keep advancing monotonically in absolute time: after `01:59` (first pass) the next run is `02:00`, not `01:00` again. The local clock rewinds, the timestamps don't. Consecutive runs are always at least the expression's interval apart (1s for `* * * * * *`, 1 min for `* * * * *`) β€” never milliseconds, even during the overlap. ## Unusual offsets node-cron handles non-hour transitions and offsets, not just the 60-minute US case: * **30-minute DST** (e.g. `Australia/Lord_Howe`, `02:00 β†’ 02:30`) β€” the gap and overlap are 30 minutes wide and handled correctly. * **45-minute base offset** (e.g. `Pacific/Chatham`, UTC+12:45) β€” schedules stay correct with no drift. * **Midnight transitions** (e.g. `America/Havana`, DST starts at `00:00`) β€” a `00:30` daily is skipped on the gap day, like any other gap time. ## Zones without DST Zones like `Asia/Tokyo`, `Etc/UTC`, or `Africa/Nairobi` have no transitions, so there are no gaps or overlaps β€” the schedule simply runs every day. node-cron applies no "corrections" to them. ## The system timezone doesn't leak in A task with an explicit `timezone` runs at the same instants no matter what the host's `TZ` is. A task set to `America/New_York` behaves identically whether the server is in SΓ£o Paulo, Tokyo, or UTC. Only tasks **without** a `timezone` use the system zone. ## Avoiding DST entirely If you never want DST to affect a schedule, run it in a **fixed-offset** zone instead of a region that observes DST. ```js // UTC: the simplest DST-free choice cron.schedule('0 3 * * *', task, { timezone: 'Etc/UTC' }); // A fixed offset that never shifts, e.g. always UTC-3 cron.schedule('0 3 * * *', task, { timezone: 'Etc/GMT+3' }); ``` ::: warning `Etc/GMT` signs are inverted In the `Etc/GMTΒ±N` zones the sign is **reversed** from what you'd expect (a POSIX quirk): `Etc/GMT+3` is **UTC-3**, and `Etc/GMT-5` is **UTC+5**. When in doubt, prefer `Etc/UTC` or a real IANA name. ::: Use a DST-observing zone (like `America/New_York`) when you want "the same local clock time year-round"; use a fixed-offset zone when you want "the same absolute spacing year-round". --- --- url: /event-listening.md description: >- Subscribe to node-cron task lifecycle events (task started/stopped/destroyed and execution started/finished/failed/missed/overlap/maxReached), each carrying a TaskContext. --- # Events & Observability Once a task is running, you'll often want to *know* what it's doing: when it ran, whether it succeeded, how long it took, whether a run was missed. node-cron exposes this through lifecycle events. Every `ScheduledTask`, whether inline or [background](/background-tasks), supports `.on()`, `.once()`, and `.off()`. Each listener receives a `TaskContext` with metadata about the task and the specific execution. ```js import cron from 'node-cron'; const task = cron.schedule('* * * * *', async () => { return doWork(); }); task.on('execution:finished', (ctx) => { console.log(`done in ${ctx.execution?.finishedAt - ctx.execution?.startedAt}ms`); }); task.on('execution:failed', (ctx) => { console.error('failed:', ctx.execution?.error?.message); }); ``` > πŸ’‘ Attach listeners **before** the task starts to avoid missing early events. With `cron.schedule` the task starts immediately, so for guaranteed coverage create it stopped with [`cron.createTask`](/task-lifecycle#creating-a-stopped-task), attach listeners, then call `.start()`. ## Available events | Event | Payload | Emitted when… | | ---------------------- | ------------- | ---------------------------------------------------------------- | | `task:started` | `TaskContext` | The task is started via `.start()`. | | `task:stopped` | `TaskContext` | The task is stopped via `.stop()`. | | `task:destroyed` | `TaskContext` | The task is destroyed via `.destroy()`. | | `task:failed` | `TaskContext` | A [background task](/background-tasks)'s daemon exited unexpectedly (crash, OOM-kill). `ctx.error` carries the reason. Inline tasks never emit this. | | `execution:started` | `TaskContext` | Right before the task function runs. | | `execution:finished` | `TaskContext` | The task function finishes successfully. | | `execution:failed` | `TaskContext` | The task function throws or rejects. | | `execution:missed` | `TaskContext` | A scheduled run was missed (blocking I/O or high CPU). | | `execution:overlap` | `TaskContext` | A run was skipped because a previous one was still going (`noOverlap`). | | `execution:maxReached` | `TaskContext` | `maxExecutions` was reached. The task is then destroyed. | | `execution:skipped` | `TaskContext` | A [`distributed`](/distributed-coordination) run was skipped on this instance. `ctx.reason` is `'not-elected'` (another instance ran it) or `'coordinator-error'` (the coordinator failed; failed closed). | ## Subscribing ```js import cron from 'node-cron'; const task = cron.createTask('* * * * *', async (ctx) => { console.log('running at', ctx.dateLocalIso); return 'done'; }); // React every time task.on('execution:finished', (ctx) => { console.log('result:', ctx.execution?.result); }); // React just once, then auto-remove task.once('task:started', () => console.log('scheduler is up')); // Stop listening const onFail = (ctx) => console.error(ctx.execution?.error); task.on('execution:failed', onFail); task.off('execution:failed', onFail); task.start(); ``` ## TaskContext Every event delivers a `TaskContext`, giving consistent access to timing and execution metadata. ```ts export type TaskContext = { date: Date; dateLocalIso: string; triggeredAt: Date; task?: ScheduledTask; execution?: Execution; reason?: 'not-elected' | 'coordinator-error'; error?: Error; }; ``` | Field | Type | Description | | -------------- | ---------------- | --------------------------------------------------------------------- | | `date` | `Date` | The time the run was scheduled for. | | `dateLocalIso` | `string` | Human-readable local timestamp, using the task's timezone. | | `triggeredAt` | `Date` | When the event was actually emitted. Useful for spotting drift. | | `task` | `ScheduledTask?` | The task instance. | | `execution` | `Execution?` | Details of the run (present for `execution:*` events). | | `reason` | `string?` | Why a run was skipped. Present only on [`execution:skipped`](/distributed-coordination#knowing-when-an-instance-skips): `'not-elected'` or `'coordinator-error'`. | | `error` | `Error?` | The daemon's exit error. Present only on `task:failed`. | ### Execution `TaskContext.execution` describes a single run of the task: ```ts export type Execution = { id: string; reason: 'invoked' | 'scheduled'; startedAt?: Date; finishedAt?: Date; error?: Error; result?: any; }; ``` | Field | Type | Description | | ------------ | --------- | ----------------------------------------------------------------- | | `id` | `string` | Unique id for this execution. | | `reason` | `string` | `'scheduled'` (fired by the schedule) or `'invoked'` (via `execute()`). | | `startedAt` | `Date?` | When the run started. | | `finishedAt` | `Date?` | When the run finished. | | `error` | `Error?` | The error, if the run failed. | | `result` | `any?` | The return value, if the run succeeded. | ## Notes * All listeners receive a `TaskContext`, even for non-execution events like `task:stopped` (where `execution` is absent). * **Background tasks** emit the same events with the same context, relayed from the worker process. They also emit `task:failed` if their daemon dies unexpectedly; node-cron does not auto-restart, so react to it if you want to recover: ```js task.on('task:failed', (ctx) => { console.error('daemon died:', ctx.error?.message); task.start(); // restart manually }); ``` * Listening to `execution:missed` also **suppresses** the default missed-execution warning, since node-cron assumes you're handling it. See [Logging](/logging#suppressing-the-missed-execution-warning). ## Next steps * **[Background Tasks](/background-tasks)**: run jobs in isolated processes (same events apply). * [Logging](/logging): route the warnings and errors behind these events through your own logger. --- --- url: /background-tasks.md description: >- Run node-cron jobs in isolated forked processes so heavy work never blocks your main event loop. Covers task files, scheduling, events, and executeTimeout. --- # Background Tasks When a job is CPU-heavy or long-running, executing it inline can block your main event loop, and node-cron will warn you about [missed executions](/logging#suppressing-the-missed-execution-warning). **Background tasks** solve this by running the job in a separate forked process (via Node's `child_process`), isolated from your application. You opt into a background task simply by passing a **file path** instead of a function to `cron.schedule`. Everything else (the lifecycle, events, and most options) works exactly the same. ## 1. Create a task file Write a module that exports a `task` function. This holds the logic to run on schedule. ::: code-group ```js [ESM] // ./tasks/my-task.js export function task() { return 'Hello from a background task!'; } ``` ```js [CommonJS] // ./tasks/my-task.js exports.task = () => { return 'Hello from a background task!'; }; ``` ::: The task function receives the same [`TaskContext`](/event-listening#taskcontext) as an inline task: ```js // ./tasks/my-task.js export function task(ctx) { console.log('scheduled for:', ctx.dateLocalIso); } ``` ## 2. Schedule it by path Pass the path where you'd pass a function. Relative paths are resolved from the file that calls `schedule`. ```js import cron from 'node-cron'; const task = cron.schedule('*/5 * * * * *', './tasks/my-task.js'); ``` node-cron forks a process, loads your task file, and schedules it there. ## Control and events A background task implements the same [`ScheduledTask`](/task-lifecycle) interface as an inline task, with one difference: because it crosses a process boundary, its control methods are **asynchronous** and return Promises. ```js import cron from 'node-cron'; const task = cron.schedule('*/5 * * * * *', './tasks/my-task.js'); await task.stop(); // terminates the child process await task.start(); // re-forks and resumes await task.destroy(); // kills the process and removes the task task.getStatus(); // synchronous: 'idle', 'running', etc. ``` It emits the [same lifecycle events](/event-listening), relayed from the worker to the parent process: ```js task.on('execution:failed', (ctx) => { console.error('background job failed:', ctx.execution?.error?.message); }); ``` ## Manual execution and `executeTimeout` Call `execute()` to run the task immediately. The task **must be started first** (the process needs to exist): ```js import cron from 'node-cron'; const task = cron.schedule('0 3 * * *', './tasks/backup.js'); const result = await task.execute(); ``` By default `execute()` waits as long as the task needs. To guard against a worker that never reports back, set `executeTimeout` (milliseconds), and `execute()` then rejects if the run doesn't finish in time: ```js import cron from 'node-cron'; const task = cron.schedule('0 3 * * *', './tasks/backup.js', { executeTimeout: 60_000, // reject if a manual execute() exceeds 60s }); ``` ## Start handshake and `startTimeout` Starting a background task forks the daemon and imports your task file. If the file doesn't load and start within `startTimeout` (default `5000` ms), `start()` rejects with a timeout error. A task file with a large dependency graph, or one that is transpiled on load, can legitimately need longer: ```js import cron from 'node-cron'; const task = cron.schedule('0 3 * * *', './tasks/backup.js', { startTimeout: 20_000, // allow a slow-loading task file more time to boot }); ``` If the file fails to load (missing file, or a runtime that can't run it, e.g. an `enum` in a `.ts` file under Node's strip-only TypeScript support), `start()` rejects with the **real** error so you can see what went wrong, rather than a generic timeout. Make sure the task file runs on its own first (e.g. `node ./tasks/backup.js`); if it needs a loader such as `tsx` or `ts-node`, the forked process needs it too, or use a compiled `.js` file. ## Limitations * **Per-task `logger` is not supported.** A logger is a function-bearing object and can't cross the process boundary. The worker forwards its events to the parent, which does the logging using the **global** logger. Use [`setLogger`](/logging#setting-a-global-logger), or call `setLogger` inside the task file itself. See [Logging](/logging#per-task-logger). * The task function lives in its **own file**; you can't pass an inline closure. * Data passed across the boundary is serialized, so event payloads contain plain data (errors are reconstructed on the parent side). ## How it works internally When a background task starts, node-cron forks a process and launches a small **daemon** that loads your task file and schedules it with the same scheduler used for inline tasks. Parent and child communicate over `child_process` messages (`task:start`, `task:stop`, `task:execute`, and event relays), keeping status and execution in sync across the boundary. Stopping or destroying the task terminates the child process. ## Next steps * **[Logging](/logging)**: essential for background tasks, since logging happens in the parent. * [Distributed Coordination](/distributed-coordination): run a background task on one instance per fire across a fleet. * [Cookbook](/cookbook): practical recipes, including a backup job. --- --- url: /distributed-coordination.md description: >- Run a node-cron task on a single instance per fire across a fleet of replicas. Covers the distributed option, the NODE_CRON_RUN env-var default, custom RunCoordinators (e.g. Redis) for HA, and the execution:skipped event. --- # Distributed Coordination The moment you run more than one copy of your app, cron gets awkward. Three replicas behind a load balancer, a PM2 cluster, a Kubernetes Deployment scaled to 4 pods, a blue/green rollout with both colors live for a minute, all of them have the same code, so all of them schedule the same job, and the nightly backup runs **four times** instead of once. `distributed: true` solves that: the task fires on exactly **one instance per scheduled time**, across the whole fleet. ```js import cron from 'node-cron'; cron.schedule('0 3 * * *', runNightlyBackup, { name: 'nightly-backup', distributed: true, }); ``` Two things are required and one is the question that drives everything else: * It is **opt-in per task** (`distributed: true`); other tasks keep running everywhere. * It needs a **`name`** (it forms the coordination key shared across instances; the auto-generated id is per-process and can't coordinate). * And it asks one question on every fire: **"should *this* instance run *this* time?"** The thing that answers is the **run coordinator**. ## The default: one designated runner Out of the box, node-cron answers that question with an environment variable, `NODE_CRON_RUN`, no extra dependencies, no Redis. You designate **one** instance as the runner: ```bash # instance A NODE_CRON_RUN=true node app.js # instances B, C, D NODE_CRON_RUN=false node app.js ``` Now the backup runs only on instance A; B, C, and D skip it. This is the simplest correct answer to "stop running my cron N times," and for many fleets it's all you need: your orchestrator already decides which pod is special (a `StatefulSet`, a single-replica `Deployment`, a dedicated worker dyno), so let it set the flag. There is **no default value**. If a `distributed` task is scheduled and `NODE_CRON_RUN` is unset (or isn't exactly `'true'`/`'false'`), node-cron **throws at schedule time**, on startup, not silently at 3 a.m.: ``` node-cron: a `distributed` task needs NODE_CRON_RUN set to 'true' or 'false'. Set it to 'true' on exactly one instance and 'false' on the others, or provide a coordinator via cron.setRunCoordinator(...). ``` This is deliberate. A silent default could only do one of two wrong things, run everywhere (the duplicates you came here to fix) or run nowhere (a backup that quietly never happens). Failing loudly on deploy is the safe choice. ::: tip This is not high availability The env-var default is a *single designated runner*. If instance A is down at 3 a.m., the backup doesn't run, B, C, and D were told `false`. For a fleet where **any** instance can take over, read on. ::: ## The guarantee With coordination in place, node-cron guarantees **no concurrent execution across instances**, effectively *once per fire* when the instances' clocks are in sync. It is **not** a hard exactly-once: under a crash-and-retry, or large clock skew between instances, a fire could still run more than once. Treat distributed tasks as **idempotent** (safe to run twice) and you're covered for the rare edge. This is a coordination primitive, not a transactional queue, if you need durable, exactly-once job semantics, reach for a queue like BullMQ. ## High availability: a custom run coordinator The env-var default trades availability for simplicity. To let **any** instance run a fire, only never two at once, you provide a **run coordinator** backed by something the whole fleet shares (typically Redis). Now there's no special instance: every replica races for each fire, exactly one wins, and if the winner is down another takes over. A coordinator is just an object that answers the question: ```ts interface RunCoordinator { // true -> this instance runs the fire identified by `key` // false -> skip it (another instance handles it) // throw -> fail closed (skip), e.g. the backend is unreachable shouldRun(key: string, ttlMs: number): boolean | Promise; // called after the run completes (success or failure); e.g. release a lock onComplete?(key: string): void | Promise; } ``` Register one globally with `setRunCoordinator` and it's used for every `distributed` task instead of the env-var default. Under the hood a lease-based coordinator turns `shouldRun` into an atomic "claim this key" and `onComplete` into a safe release, so for the fire keyed `nightly-backup:2026-06-17T03:00:00.000Z`, the first instance to claim it wins and the rest get `false`. ### The Redis coordinator The official Redis implementation ships as a separate package, [`@node-cron/redis-coordinator`](https://www.npmjs.com/package/@node-cron/redis-coordinator), so the core stays dependency-free. You install it alongside `node-cron` and **the Redis client you already use** (it supports both [`ioredis`](https://github.com/redis/ioredis) and [`node-redis`](https://github.com/redis/node-redis) v4, and auto-detects which one you passed): ::: code-group ```bash [node-redis] npm install @node-cron/redis-coordinator node-cron redis ``` ```bash [ioredis] npm install @node-cron/redis-coordinator node-cron ioredis ``` ::: It needs **node-cron >= 4.4.1** (the peer dependency) and has **zero runtime dependencies** of its own: you pass in a client you already created and connected, and the coordinator just uses it (your TLS, Sentinel, Cluster, auth, and retry config stay yours). ```js import { createClient } from 'redis'; import cron, { setRunCoordinator } from 'node-cron'; import { RedisLockCoordinator } from '@node-cron/redis-coordinator'; const redis = createClient(); // your client, your connection await redis.connect(); setRunCoordinator(new RedisLockCoordinator(redis)); // Deploy on N instances: only one runs each 3am fire, and it survives the loss of any node. cron.schedule('0 3 * * *', runNightlyBackup, { name: 'nightly-backup', distributed: true, distributedLease: 5 * 60_000, // the backup can take up to ~5 minutes }); ``` With `ioredis` it's the same, just hand in the client you have: ```js import Redis from 'ioredis'; setRunCoordinator(new RedisLockCoordinator(new Redis())); ``` Options: `keyPrefix` (default `node-cron:lock:`), `clientType` (`'auto'` | `'ioredis'` | `'node-redis'`), and a `logger`. It also exposes a `healthCheck()` that compares the local clock to the Redis server clock, see [clock skew](#clock-skew) below. ::: tip Other backends `@node-cron/redis-coordinator` is just one implementation of the `RunCoordinator` interface above. Any object with `shouldRun`/`onComplete` works, so you can back coordination with Postgres, etcd, or anything your fleet shares. ::: ### Per-task coordinator A coordinator can also be set on a single task, overriding the global one: ```js cron.schedule('*/5 * * * *', syncInventory, { name: 'sync-inventory', distributed: true, runCoordinator: myCoordinator, // wins over setRunCoordinator() and the env default }); ``` Resolution order: per-task `runCoordinator` β†’ global `setRunCoordinator` β†’ the env-var default. ## Knowing when an instance skips When an instance is **not** the one chosen to run a fire, it emits [`execution:skipped`](/event-listening) instead of running. The context carries a `reason`: ```js task.on('execution:skipped', (ctx) => { if (ctx.reason === 'coordinator-error') { // the coordinator failed (e.g. Redis down) and we failed closed. // this is the one to alert on: the fire may not have run anywhere. alert('cron coordination is failing', ctx); } // ctx.reason === 'not-elected' is the healthy case: another instance ran it. }); ``` | `reason` | Meaning | | -------------------- | ---------------------------------------------------------------------------------------- | | `'not-elected'` | Healthy. Another instance was chosen for this fire. | | `'coordinator-error'`| The coordinator threw (e.g. the backend was unreachable). node-cron **failed closed** and skipped, so the fire may not have run on any instance. Alert on this. | The instance that *does* run emits the normal [`execution:started` β†’ `execution:finished`](/event-listening) sequence, so "did this instance run it?" is just "did I get `execution:started`?", no extra event needed. ## `distributedLease` Lease-based coordinators (like a Redis lock) hold the claim for a safety window in case the winner crashes mid-run without releasing it. `distributedLease` (ms, default `30000`) sets that lease, and it **must exceed the task's run time**, or the lease can expire mid-run and a late-arriving instance (delayed by clock skew, GC, or a blocked event loop) could re-acquire the expired key and start a second copy. The env-var default ignores it. ```js cron.schedule('0 3 * * *', runNightlyBackup, { name: 'nightly-backup', distributed: true, distributedLease: 5 * 60_000, // the backup can take up to ~5 minutes }); ``` ::: warning Account for jitter The lease is acquired at the scheduled time, **before** any [`maxRandomDelay`](/scheduling-options) jitter is applied (the jitter only runs on the instance that won the lock, never affecting which instance wins). So the key stays claimed for `jitter + run time`, not just the run time. When you combine `distributed` with `maxRandomDelay`, size the lease for both: ``` distributedLease > maxRandomDelay + the task's max run time ``` Otherwise the lease can expire while the winner is still waiting out its jitter or running, reopening the double-run window above. ::: ## Clock skew Coordination keys are built from the **scheduled time** (`name:fireTimeISO`), so every instance must agree on what time it is. If two instances' clocks drift apart, they compute different keys for the "same" fire, claim different locks, and both run, the no-concurrent guarantee quietly degrades. Keep your fleet on NTP. To catch drift before it bites, `@node-cron/redis-coordinator` exposes a `healthCheck()` that compares the local clock to the Redis server clock (a shared reference) and reports the skew: ```js const coordinator = new RedisLockCoordinator(redis); cron.setRunCoordinator(coordinator); const { ok, driftMs } = await coordinator.healthCheck(); // default threshold 1000ms if (!ok) { console.warn(`clock skew vs Redis is ${driftMs}ms; distributed coordination may be unreliable`); } ``` Run it at startup (or on a health endpoint) to surface a misconfigured clock as an alert rather than a duplicate run. ## Background tasks work too `distributed` works for [background tasks](/background-tasks) exactly as for inline ones, you set the coordinator in your main process and it applies transparently. Internally the forked daemon can't hold the coordinator (it lives across a process boundary), so it asks the parent over IPC, and the parent runs the real coordinator. The shared backend still arbitrates across the fleet; you don't configure anything extra. ```js // in your main process setRunCoordinator(new RedisLockCoordinator(redis)); cron.schedule('0 3 * * *', './tasks/backup.js', { name: 'nightly-backup', distributed: true, }); ``` ## A note on `maxExecutions` [`maxExecutions`](/scheduling-options) is counted **per instance**. With a per-fire coordinator (the HA case), each instance only counts the fires it won, so the fleet total can exceed your limit. With the single-runner env-var default it behaves as expected, only the designated instance runs and counts. ## How it works internally On each fire of a `distributed` task, node-cron builds a key from the task's `name` and the exact scheduled time (`name:fireTimeISO`), so every instance computes the **same** key for the **same** fire. It calls `shouldRun(key, ttl)`; on `true` it runs the task and then calls `onComplete(key)`; on `false` (or a thrown error) it emits `execution:skipped` and moves on. The coordinator is where cross-instance agreement happens, node-cron itself stays a scheduler. ## Next steps * **[Events & Observability](/event-listening)**: handle `execution:skipped` and the rest of the lifecycle. * [Background Tasks](/background-tasks): run the work in an isolated process; coordination still applies. * [Scheduling Options](/scheduling-options): the full list of options, including `distributed`, `runCoordinator`, and `distributedLease`. --- --- url: /logging.md description: >- Route node-cron's internal messages through your own logger with setLogger or the per-task logger option, integrate winston/pino, silence output, and control the missed-execution warning. --- # Logging node-cron writes a few internal messages, most notably a warning when a scheduled execution is missed (usually caused by blocking I/O or high CPU in the same process). You can route these messages through your own logger and control the missed-execution warning. ## The `Logger` interface A logger is any object that implements these four methods. You don't need to extend or `implements` anything; any matching object works (structural typing). ```ts interface Logger { info(message: string): void; warn(message: string): void; error(message: string | Error, err?: Error): void; debug(message: string | Error, err?: Error): void; } ``` > πŸ›ˆ `error` and `debug` may receive either a `string` or an `Error`, so handle > both in your adapter. ## Setting a global logger Use `setLogger` to replace the built-in console logger for the whole module: ```js import cron, { setLogger } from 'node-cron'; setLogger({ info: (msg) => myLogger.info(msg), warn: (msg) => myLogger.warn(msg), error: (msg) => myLogger.error(msg), debug: (msg) => myLogger.debug(msg), }); ``` In CommonJS: ```js const cron = require('node-cron'); cron.setLogger({ /* ... */ }); ``` ### Using winston / pino Both expose `info`/`warn`/`error`/`debug`, so they are almost a drop-in: ```js import pino from 'pino'; import { setLogger } from 'node-cron'; const log = pino(); setLogger({ info: (m) => log.info(m), warn: (m) => log.warn(m), error: (m, e) => log.error(e ?? m), debug: (m) => log.debug(m), }); ``` ### Silencing all output Pass a logger whose methods do nothing: ```js import { setLogger } from 'node-cron'; const noop = () => {}; setLogger({ info: noop, warn: noop, error: noop, debug: noop }); ``` ## Per-task logger You can also override the logger for a single task with the `logger` option. It takes precedence over the global logger for that task: ```js const task = cron.schedule('* * * * *', () => {}, { logger: myTaskLogger, }); ``` > πŸ›ˆ The per-task `logger` is **not** supported for [Background Tasks](/background-tasks), > because it cannot cross the worker process boundary. For background tasks, use > the global `setLogger` (the parent process does the logging from the worker's > events) or call `setLogger` inside the task file itself. ## Suppressing the "missed execution" warning When a scheduled execution is missed, node-cron logs: ``` [NODE-CRON] [WARN] missed execution at