463 lines
21 KiB
Plaintext
463 lines
21 KiB
Plaintext
---
|
||
title: Cron
|
||
description: Schedule and parse cron jobs with Bun
|
||
---
|
||
|
||
Bun has built-in support for cron — parse expressions, run a callback on a schedule inside your process, or register OS-level jobs that survive restarts.
|
||
|
||
## Quickstart
|
||
|
||
**Run a callback on a schedule in the current process:**
|
||
|
||
```ts
|
||
Bun.cron("0 * * * *", async () => {
|
||
await cleanupTempFiles();
|
||
});
|
||
```
|
||
|
||
**Parse a cron expression to find the next matching time:**
|
||
|
||
```ts
|
||
// Next weekday at 9:30 AM local time
|
||
const next = Bun.cron.parse("30 9 * * MON-FRI");
|
||
```
|
||
|
||
**Register an OS-level cron job that runs a script on a schedule:**
|
||
|
||
```ts
|
||
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");
|
||
```
|
||
|
||
---
|
||
|
||
## `Bun.cron.parse()`
|
||
|
||
Parse a cron expression and return the next matching `Date` in the system's local time zone.
|
||
|
||
```ts
|
||
const next = Bun.cron.parse("*/15 * * * *");
|
||
console.log(next); // => next quarter-hour boundary
|
||
```
|
||
|
||
### Parameters
|
||
|
||
| Parameter | Type | Description |
|
||
| -------------- | ----------------- | ------------------------------------------------------------------------------ |
|
||
| `expression` | `string` | A 5-field cron expression or predefined nickname |
|
||
| `relativeDate` | `Date \| number` | Starting point for the search (defaults to `Date.now()`) |
|
||
| `options` | `{ tz?: string }` | IANA time-zone name to interpret the schedule in (defaults to the system zone) |
|
||
|
||
### Returns
|
||
|
||
`Date | null` — the next matching time, or `null` if no match exists within 8 years (for example, February 30th).
|
||
|
||
### Chaining calls
|
||
|
||
Call `parse()` repeatedly to get a sequence of upcoming times:
|
||
|
||
```ts
|
||
let cursor: Date | number = Date.now();
|
||
for (let i = 0; i < 3; i++) {
|
||
cursor = Bun.cron.parse("0 * * * *", cursor)!;
|
||
console.log(cursor.toLocaleString()); // next three top-of-hour boundaries
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Cron expression syntax
|
||
|
||
Standard 5-field format: `minute hour day-of-month month day-of-week`
|
||
|
||
| Field | Values | Special characters |
|
||
| ------------ | ----------------------- | ------------------ |
|
||
| Minute | `0`–`59` | `*` `,` `-` `/` |
|
||
| Hour | `0`–`23` | `*` `,` `-` `/` |
|
||
| Day of month | `1`–`31` | `*` `,` `-` `/` |
|
||
| Month | `1`–`12` or `JAN`–`DEC` | `*` `,` `-` `/` |
|
||
| Day of week | `0`–`7` or `SUN`–`SAT` | `*` `,` `-` `/` |
|
||
|
||
### Special characters
|
||
|
||
| Character | Description | Example |
|
||
| --------- | ----------- | ------------------------------------- |
|
||
| `*` | All values | `* * * * *` — every minute |
|
||
| `,` | List | `1,15 * * * *` — minute 1 and 15 |
|
||
| `-` | Range | `9-17 * * * *` — minutes 9 through 17 |
|
||
| `/` | Step | `*/15 * * * *` — every 15 minutes |
|
||
|
||
### Named values
|
||
|
||
Month and weekday fields accept case-insensitive names:
|
||
|
||
```ts
|
||
// 3-letter abbreviations
|
||
Bun.cron.parse("0 9 * * MON-FRI"); // weekdays
|
||
Bun.cron.parse("0 0 1 JAN,JUN *"); // January and June
|
||
|
||
// Full names
|
||
Bun.cron.parse("0 9 * * Monday-Friday");
|
||
Bun.cron.parse("0 0 1 January *");
|
||
```
|
||
|
||
Both `0` and `7` mean Sunday in the weekday field.
|
||
|
||
### Predefined nicknames
|
||
|
||
| Nickname | Equivalent | Description |
|
||
| ----------------------- | ----------- | ------------------------- |
|
||
| `@yearly` / `@annually` | `0 0 1 1 *` | Once a year (January 1st) |
|
||
| `@monthly` | `0 0 1 * *` | Once a month (1st day) |
|
||
| `@weekly` | `0 0 * * 0` | Once a week (Sunday) |
|
||
| `@daily` / `@midnight` | `0 0 * * *` | Once a day (midnight) |
|
||
| `@hourly` | `0 * * * *` | Once an hour |
|
||
|
||
```ts
|
||
const next = Bun.cron.parse("@daily");
|
||
console.log(next); // => next local midnight
|
||
```
|
||
|
||
### Time zone
|
||
|
||
Bun interprets schedules in the system's **local time zone**, the same way crontab, launchd, and Windows Task Scheduler read them. The OS-level form and the in-process callback form fire at the same wall-clock time.
|
||
|
||
To override, pass an IANA time-zone name as `{ tz }` to `Bun.cron.parse()` or the in-process `Bun.cron(schedule, handler, options)`:
|
||
|
||
```ts
|
||
// 09:00 UTC, regardless of the server's TZ
|
||
Bun.cron.parse("0 9 * * *", Date.now(), { tz: "UTC" });
|
||
|
||
// Fire at 09:00 New York time
|
||
Bun.cron("0 9 * * *", handler, { tz: "America/New_York" });
|
||
```
|
||
|
||
DST transitions:
|
||
|
||
- **Spring-forward** — a schedule that lands in the missing hour fires that day, shifted forward by the gap (e.g. `30 2 * * *` runs at 3:30 on the spring-forward day). For multi-minute patterns inside the gap (`*/15 2 * * *`), only the first match fires.
|
||
- **Fall-back** — a fixed-time schedule in the duplicated hour (`30 1 * * *`) fires once, at the first occurrence. A schedule whose minute or hour field is `*` (`0 * * * *`, `* * * * *`) fires through **both** occurrences — once per real-time minute, matching crontab on Linux.
|
||
|
||
### Day-of-month and day-of-week interaction
|
||
|
||
When you specify **both** day-of-month and day-of-week (neither is `*`), the expression matches when **either** condition is true. This follows the [POSIX cron](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html) standard.
|
||
|
||
```ts
|
||
// Fires on the 15th of every month OR every Friday
|
||
Bun.cron.parse("0 0 15 * FRI");
|
||
```
|
||
|
||
When you specify only one (the other is `*`), Bun uses only that field for matching.
|
||
|
||
---
|
||
|
||
## `Bun.cron(schedule, handler)` — in-process
|
||
|
||
Run a callback on a cron schedule inside the current process.
|
||
|
||
```ts
|
||
const job = Bun.cron("*/5 * * * *", async () => {
|
||
await syncToDatabase();
|
||
});
|
||
```
|
||
|
||
In-process scheduling is the lightweight option for long-running servers and workers. It requires no system cron daemon, works the same on every platform, and shares state (database pools, caches, module-level variables) between invocations.
|
||
|
||
| | In-process | [OS-level](#bun-cron-path-schedule-title-os-level) |
|
||
| ---------------------------- | -------------------------------- | -------------------------------------------------- |
|
||
| Survives process exit/reboot | No | Yes |
|
||
| Shared state between runs | Yes | No (fresh process each time) |
|
||
| Platform requirements | None | crontab / launchd / Task Scheduler |
|
||
| Windows expression limits | None | [48-trigger cap](#trigger-limit) |
|
||
| Return type | [`CronJob`](#the-cronjob-handle) | `Promise<void>` |
|
||
|
||
### Parameters
|
||
|
||
| Parameter | Type | Description |
|
||
| ---------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `schedule` | `string` | A [cron expression](#cron-expression-syntax) or nickname like `"@hourly"`. |
|
||
| `handler` | `(this: CronJob) => unknown` | Called on each fire. May return a Promise; Bun schedules the next fire only once it settles. Inside a `function` callback, `this` is the `CronJob` (so `this.stop()` works). |
|
||
| `options` | `{ tz?: string }` | IANA time-zone name to interpret the schedule in (defaults to the system zone). |
|
||
|
||
Returns a [`CronJob`](#the-cronjob-handle) synchronously. Throws a `TypeError` if the expression is invalid, the time-zone name is unknown, or the expression has no future occurrences, like `"0 0 30 2 *"` (February 30th).
|
||
|
||
### No-overlap guarantee
|
||
|
||
Bun computes the next fire time only after the handler and any returned `Promise` settle. If your handler takes 90 seconds and the schedule is `* * * * *`, the second fire is the first minute boundary _after_ the handler finishes, not 60 seconds after the first fire. Invocations never stack.
|
||
|
||
### Error handling
|
||
|
||
Errors match `setTimeout` semantics:
|
||
|
||
- A synchronous `throw` emits `process.on("uncaughtException")`.
|
||
- A rejected returned `Promise` emits `process.on("unhandledRejection")`.
|
||
|
||
Without a listener, the process exits with code `1`. With a listener, the job keeps running — it does not stop on the first failure.
|
||
|
||
```ts
|
||
process.on("unhandledRejection", err => log.error("cron failed:", err));
|
||
|
||
Bun.cron("* * * * *", async () => {
|
||
await mightThrow(); // logged and retried next minute
|
||
});
|
||
```
|
||
|
||
### `bun --hot`
|
||
|
||
Under `bun --hot`, Bun stops all in-process cron jobs immediately before the module graph re-evaluates. Every `Bun.cron()` call still in your source then re-registers. Editing the schedule, editing the handler, or deleting the line entirely all take effect on save without leaking timers.
|
||
|
||
### The `CronJob` handle
|
||
|
||
```ts
|
||
using job = Bun.cron("0 * * * *", () => {});
|
||
|
||
job.cron; // => "0 * * * *"
|
||
job.stop(); // cancel — the handler will not fire again
|
||
job.unref(); // allow the process to exit even while scheduled
|
||
job.ref(); // keep the process alive (default)
|
||
```
|
||
|
||
`CronJob` is [`Disposable`](https://github.com/tc39/proposal-explicit-resource-management) — `using job = Bun.cron(...)` auto-stops at scope exit. `stop()`, `ref()`, and `unref()` all return the job for chaining.
|
||
|
||
### Fake timers
|
||
|
||
In-process cron honors `jest.useFakeTimers()`. `setSystemTime()`, `advanceTimersByTime()`, and `runAllTimers()` control when it fires, so you can test scheduled callbacks without waiting on the real clock.
|
||
|
||
---
|
||
|
||
## `Bun.cron(path, schedule, title)` — OS-level
|
||
|
||
Register an OS-level cron job that runs a JavaScript/TypeScript module on a schedule.
|
||
|
||
```ts
|
||
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");
|
||
```
|
||
|
||
### Parameters
|
||
|
||
| Parameter | Type | Description |
|
||
| ---------- | -------- | ---------------------------------------------------------- |
|
||
| `path` | `string` | Path to the script (resolved relative to caller) |
|
||
| `schedule` | `string` | Cron expression or nickname |
|
||
| `title` | `string` | Unique job identifier (alphanumeric, hyphens, underscores) |
|
||
|
||
Re-registering with the same `title` overwrites the existing job in-place. Bun replaces the old schedule instead of duplicating it.
|
||
|
||
```ts
|
||
await Bun.cron("./worker.ts", "0 * * * *", "my-job"); // every hour
|
||
await Bun.cron("./worker.ts", "*/15 * * * *", "my-job"); // replaces: every 15 min
|
||
```
|
||
|
||
### The `scheduled()` handler
|
||
|
||
The registered script must export a default object with a `scheduled()` method, following the [Cloudflare Workers Cron Triggers API](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/):
|
||
|
||
```ts worker.ts
|
||
export default {
|
||
scheduled(controller: Bun.CronController) {
|
||
console.log(controller.cron); // "30 2 * * 1"
|
||
console.log(controller.type); // "scheduled"
|
||
console.log(controller.scheduledTime); // 1737340201847 (Date.now() at invocation)
|
||
},
|
||
};
|
||
```
|
||
|
||
The handler can be `async`. Bun waits for the returned promise to settle before exiting.
|
||
|
||
---
|
||
|
||
## How it works per platform
|
||
|
||
### Linux
|
||
|
||
Bun uses [crontab](https://man7.org/linux/man-pages/man5/crontab.5.html) to register jobs. Bun stores each job as a line in your user's crontab with a `# bun-cron: <title>` marker comment above it.
|
||
|
||
The crontab entry looks like:
|
||
|
||
```
|
||
<schedule> '<bun-path>' run --cron-title=<title> --cron-period='<schedule>' '<script-path>'
|
||
```
|
||
|
||
When the cron daemon fires the job, Bun imports your module and calls the `scheduled()` handler.
|
||
|
||
**Viewing registered jobs:**
|
||
|
||
```sh
|
||
crontab -l
|
||
```
|
||
|
||
**Logs:** On Linux, cron output goes to the system log. Check with:
|
||
|
||
```sh
|
||
# systemd-based (Ubuntu, Fedora, Arch, etc.)
|
||
journalctl -u cron # or crond on some distros
|
||
journalctl -u cron --since "1 hour ago"
|
||
|
||
# syslog-based (older systems)
|
||
grep CRON /var/log/syslog
|
||
```
|
||
|
||
To capture stdout/stderr to a file, redirect output in the crontab entry directly, or add logging inside your `scheduled()` handler.
|
||
|
||
**Manually uninstalling without code:**
|
||
|
||
```sh
|
||
# Edit your crontab and remove the "# bun-cron: <title>" comment
|
||
# and the command line below it
|
||
crontab -e
|
||
|
||
# Or remove ALL bun cron jobs at once by filtering them out:
|
||
crontab -l | grep -v "# bun-cron:" | grep -v "\-\-cron-title=" | crontab -
|
||
```
|
||
|
||
### macOS
|
||
|
||
Bun uses [launchd](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html) to register jobs. Bun installs each job as a plist file at:
|
||
|
||
```
|
||
~/Library/LaunchAgents/bun.cron.<title>.plist
|
||
```
|
||
|
||
The plist uses `StartCalendarInterval` to define the schedule. Complex patterns with ranges, lists, or steps are supported. Bun expands them into multiple `StartCalendarInterval` dicts as a Cartesian product.
|
||
|
||
**Viewing registered jobs:**
|
||
|
||
```sh
|
||
launchctl list | grep bun.cron
|
||
```
|
||
|
||
**Logs:** stdout and stderr are written to:
|
||
|
||
```
|
||
/tmp/bun.cron.<title>.stdout.log
|
||
/tmp/bun.cron.<title>.stderr.log
|
||
```
|
||
|
||
For example, a job titled `weekly-report`:
|
||
|
||
```sh
|
||
cat /tmp/bun.cron.weekly-report.stdout.log
|
||
tail -f /tmp/bun.cron.weekly-report.stderr.log
|
||
```
|
||
|
||
**Manually uninstalling without code:**
|
||
|
||
```sh
|
||
# Unload the job from launchd
|
||
launchctl bootout gui/$(id -u)/bun.cron.<title>
|
||
|
||
# Delete the plist file
|
||
rm ~/Library/LaunchAgents/bun.cron.<title>.plist
|
||
|
||
# Example for a job titled "weekly-report":
|
||
launchctl bootout gui/$(id -u)/bun.cron.weekly-report
|
||
rm ~/Library/LaunchAgents/bun.cron.weekly-report.plist
|
||
```
|
||
|
||
### Windows
|
||
|
||
Bun uses [Windows Task Scheduler](https://learn.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-start-page) with XML-based task definitions. Bun registers each job as a scheduled task named `bun-cron-<title>` using [`CalendarTrigger`](https://learn.microsoft.com/en-us/windows/win32/taskschd/taskschedulerschema-calendartrigger-triggergroup-element) elements and [`Repetition`](https://learn.microsoft.com/en-us/windows/win32/taskschd/taskschedulerschema-repetition-triggerbasetype-element) patterns.
|
||
|
||
Most cron expressions are fully supported, including `@daily`, `@weekly`, `@monthly`, `@yearly`, ranges (`1-5`), lists (`1,15`), named days/months, and day-of-month patterns.
|
||
|
||
#### User context
|
||
|
||
Bun registers tasks with the [`S4U` (Service-for-User)](https://learn.microsoft.com/en-us/windows/win32/taskschd/taskschedulerschema-logontype-simpletype) logon type, which runs jobs as the registering user even when not logged in. Linux `crontab` behaves the same way. No password is stored.
|
||
|
||
TCP/IP networking (`fetch()`, HTTP, WebSocket, database connections) works normally. The only restriction is that S4U tasks cannot access [Windows-authenticated network resources](https://learn.microsoft.com/en-us/windows/win32/taskschd/security-contexts-for-running-tasks) (SMB file shares, mapped drives, Kerberos/NTLM services).
|
||
|
||
On some headless servers and CI environments, the current user's [Security Identifier (SID)](https://learn.microsoft.com/en-us/windows/security/identity-protection/access-control/security-identifiers) cannot be resolved, for example with service accounts created by [NSSM](https://nssm.cc/) or similar tools. In that case, `Bun.cron()` fails with an error explaining the issue. To work around this, either run Bun as a regular user account, or create the scheduled task manually with `schtasks /create /xml <file> /tn <name> /ru SYSTEM /f`.
|
||
|
||
#### Trigger limit
|
||
|
||
<Warning>
|
||
Windows Task Scheduler enforces a limit of [48 triggers per
|
||
task](https://learn.microsoft.com/en-us/windows/win32/taskschd/taskschedulerschema-triggers-tasktype-element) (the
|
||
`CalendarTrigger` element has
|
||
[`maxOccurs="48"`](https://learn.microsoft.com/en-us/windows/win32/taskschd/taskschedulerschema-calendartrigger-triggergroup-element)).
|
||
Some cron expressions that work on Linux and macOS exceed this limit on Windows. When a pattern exceeds the limit,
|
||
`Bun.cron()` rejects it with an error message.
|
||
</Warning>
|
||
|
||
**Expressions that work on all platforms:**
|
||
|
||
| Pattern | Trigger strategy | Count |
|
||
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
||
| `*/5 * * * *` | Single trigger with [`Repetition`](https://learn.microsoft.com/en-us/windows/win32/taskschd/taskschedulerschema-repetition-triggerbasetype-element) (PT5M) | 1 |
|
||
| `*/15 * * * *` | Single trigger with Repetition (PT15M) | 1 |
|
||
| `0 9 * * MON-FRI` | One `CalendarTrigger` listing all five weekdays | 1 |
|
||
| `0,30 9-17 * * *` | 2 minutes × 9 hours | 18 |
|
||
| `@daily`, `@weekly`, `@monthly`, `@yearly` | Single trigger | 1 |
|
||
|
||
**Expressions that fail on Windows** (but work on Linux and macOS):
|
||
|
||
| Pattern | Why | Trigger count |
|
||
| ----------------- | --------------------------------------------- | ------------- |
|
||
| `*/7 * * * *` | 9 minute values × 24 hours | 216 |
|
||
| `*/8 * * * *` | 8 minute values × 24 hours | 192 |
|
||
| `*/9 * * * *` | 7 minute values × 24 hours | 168 |
|
||
| `*/11 * * * *` | 6 minute values × 24 hours | 144 |
|
||
| `*/13 * * * *` | 5 minute values × 24 hours | 120 |
|
||
| `*/15 * * 6 *` | Month restriction prevents Repetition: 4 × 24 | 96 |
|
||
| `0,30 * 15 * FRI` | OR-split doubles triggers: 2 × 24 × 2 | 96 |
|
||
|
||
The key factor is whether the expression can use a [`Repetition`](https://learn.microsoft.com/en-us/windows/win32/taskschd/taskschedulerschema-repetition-triggerbasetype-element) interval (single trigger) or must expand to individual `CalendarTrigger` elements. Minute steps that **evenly divide 60** (`*/1`, `*/2`, `*/3`, `*/4`, `*/5`, `*/6`, `*/10`, `*/12`, `*/15`, `*/20`, `*/30`) use Repetition only when the other four fields are unrestricted (`*`). Steps that don't divide 60 (`*/7`, `*/8`, `*/9`, `*/11`, `*/13`, etc.) must be expanded, and with 24 hours active, the count quickly exceeds 48.
|
||
|
||
To work around it, simplify the expression or restrict the hour range:
|
||
|
||
```ts
|
||
// ❌ Fails on Windows: */7 with all hours = 216 triggers
|
||
await Bun.cron("./job.ts", "*/7 * * * *", "my-job");
|
||
|
||
// ✅ Works: restrict to specific hours (9 values × 5 hours = 45 triggers)
|
||
await Bun.cron("./job.ts", "*/7 9-13 * * *", "my-job");
|
||
|
||
// ✅ Works: use a divisor of 60 instead (Repetition, 1 trigger)
|
||
await Bun.cron("./job.ts", "*/5 * * * *", "my-job");
|
||
```
|
||
|
||
#### Windows containers
|
||
|
||
<Warning>
|
||
`Bun.cron()` is not supported in Windows Docker containers. The Task Scheduler service is not running in `servercore`
|
||
or `nanoserver` images. Use an in-process scheduler for containerized workloads.
|
||
</Warning>
|
||
|
||
**Viewing registered jobs:**
|
||
|
||
```powershell
|
||
schtasks /query /tn "bun-cron-<title>"
|
||
|
||
# List all bun cron tasks
|
||
schtasks /query | findstr "bun-cron-"
|
||
```
|
||
|
||
**Manually uninstalling without code:**
|
||
|
||
```powershell
|
||
schtasks /delete /tn "bun-cron-<title>" /f
|
||
|
||
# Example:
|
||
schtasks /delete /tn "bun-cron-weekly-report" /f
|
||
```
|
||
|
||
Or open **Task Scheduler** (taskschd.msc), find the task named `bun-cron-<title>`, right-click, and delete it.
|
||
|
||
---
|
||
|
||
## `Bun.cron.remove()`
|
||
|
||
Remove a previously registered cron job by its title. Works on all platforms.
|
||
|
||
```ts
|
||
await Bun.cron.remove("weekly-report");
|
||
```
|
||
|
||
This reverses what `Bun.cron()` did:
|
||
|
||
| Platform | What `remove()` does |
|
||
| -------- | -------------------------------------------------------- |
|
||
| Linux | Edits crontab to remove the entry and its marker comment |
|
||
| macOS | Runs `launchctl bootout` and deletes the plist file |
|
||
| Windows | Runs `schtasks /delete` to remove the scheduled task |
|
||
|
||
Removing a job that doesn't exist resolves without error.
|