> ## Documentation Index
> Fetch the complete documentation index at: https://bun.ll1025.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# Cron

> 使用 Bun 调度和解析 cron 任务

Bun 内置了对 cron 的支持 — 解析表达式、在进程内按计划运行回调，或注册可在重启后保持的 OS 级别任务。

## 快速入门

**在当前进程中按计划运行回调：**

```ts theme={null}
Bun.cron("0 * * * *", async () => {
  await cleanupTempFiles();
});
```

**解析 cron 表达式以查找下一个匹配时间：**

```ts theme={null}
// 下一个工作日上午 9:30 UTC
const next = Bun.cron.parse("30 9 * * MON-FRI");
```

**注册一个按计划运行脚本的 OS 级别 cron 任务：**

```ts theme={null}
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");
```

***

## `Bun.cron.parse()`

解析 cron 表达式并返回 UTC 时区的下一个匹配 `Date`。

```ts theme={null}
const next = Bun.cron.parse("*/15 * * * *");
console.log(next); // => 下一个刻钟边界
```

### 参数

| 参数             | 类型               | 说明                       |
| -------------- | ---------------- | ------------------------ |
| `expression`   | `string`         | 5 字段 cron 表达式或预定义昵称      |
| `relativeDate` | `Date \| number` | 搜索的起始点（默认为 `Date.now()`） |

### 返回值

`Date | null` — 下一个匹配时间，如果 8 年内没有匹配则返回 `null`（例如，2 月 30 日）。

### 链式调用

重复调用 `parse()` 以获取一系列即将到来的时间：

```ts theme={null}
let cursor: Date | number = Date.now();
for (let i = 0; i < 3; i++) {
  cursor = Bun.cron.parse("0 * * * *", cursor)!;
  console.log(cursor.toLocaleString()); // 接下来三个整点边界
}
```

***

## Cron 表达式语法

标准 5 字段格式：`minute hour day-of-month month day-of-week`

| 字段    | 值                      | 特殊字符            |
| ----- | ---------------------- | --------------- |
| 分钟    | `0`–`59`               | `*` `,` `-` `/` |
| 小时    | `0`–`23`               | `*` `,` `-` `/` |
| 日期（月） | `1`–`31`               | `*` `,` `-` `/` |
| 月份    | `1`–`12` 或 `JAN`–`DEC` | `*` `,` `-` `/` |
| 星期    | `0`–`7` 或 `SUN`–`SAT`  | `*` `,` `-` `/` |

### 特殊字符

| 字符  | 说明  | 示例                           |
| --- | --- | ---------------------------- |
| `*` | 所有值 | `* * * * *` — 每分钟            |
| `,` | 列表  | `1,15 * * * *` — 第 1 和 15 分钟 |
| `-` | 范围  | `9-17 * * * *` — 第 9 到 17 分钟 |
| `/` | 步长  | `*/15 * * * *` — 每 15 分钟     |

### 命名值

月份和星期字段接受不区分大小写的名称：

```ts theme={null}
// 3 字母缩写
Bun.cron.parse("0 9 * * MON-FRI"); // 工作日
Bun.cron.parse("0 0 1 JAN,JUN *"); // 一月和六月

// 完整名称
Bun.cron.parse("0 9 * * Monday-Friday");
Bun.cron.parse("0 0 1 January *");
```

星期字段中，`0` 和 `7` 都表示星期日。

### 预定义昵称

| 昵称                      | 等效表达式       | 说明            |
| ----------------------- | ----------- | ------------- |
| `@yearly` / `@annually` | `0 0 1 1 *` | 每年一次（1 月 1 日） |
| `@monthly`              | `0 0 1 * *` | 每月一次（第 1 天）   |
| `@weekly`               | `0 0 * * 0` | 每周一次（星期日）     |
| `@daily` / `@midnight`  | `0 0 * * *` | 每天一次（午夜）      |
| `@hourly`               | `0 * * * *` | 每小时一次         |

```ts theme={null}
const next = Bun.cron.parse("@daily");
console.log(next); // => 下一个 UTC 午夜
```

### 时区

`Bun.cron.parse()` 和进程内 `Bun.cron(schedule, handler)` 按 **UTC** 解释调度。无需处理夏令时 — `0 9 * * *` 始终表示 9:00 UTC。

OS 级别的 `Bun.cron(path, schedule, title)` 使用**系统的本地时区**，因为 crontab、launchd 和 Windows Task Scheduler 就是这样工作的。要使两种形式一致，请使用 `TZ=UTC` 运行进程。

### 日期和星期的交互

当**同时**指定了日期（月）和星期（两者都不是 `*`）时，表达式在**任一**条件为真时匹配。这遵循 [POSIX cron](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html) 标准。

```ts theme={null}
// 每月 15 日或每个星期五触发
Bun.cron.parse("0 0 15 * FRI");
```

当只指定其中一个（另一个为 `*`）时，仅使用该字段进行匹配。

***

## `Bun.cron(schedule, handler)` — 进程内

在当前进程内按 cron 计划运行回调。

```ts theme={null}
const job = Bun.cron("*/5 * * * *", async () => {
  await syncToDatabase();
});
```

进程内调度是长期运行的服务器和 worker 的轻量级选择 — 无需系统 cron 守护进程，在所有平台上工作方式相同，并在调用之间共享状态（数据库连接池、缓存、模块级变量）。

|               | 进程内                              | [OS 级别](#bun-cron-path-schedule-title-os-level) |
| ------------- | -------------------------------- | ----------------------------------------------- |
| 进程退出/重启后持续存在  | 否                                | 是                                               |
| 运行之间共享状态      | 是                                | 否（每次是全新进程）                                      |
| 平台要求          | 无                                | crontab / launchd / Task Scheduler              |
| Windows 表达式限制 | 无                                | [48 触发器上限](#trigger-limit)                      |
| 返回类型          | [`CronJob`](#the-cronjob-handle) | `Promise<void>`                                 |

### 参数

| 参数         | 类型                           | 说明                                                                                               |
| ---------- | ---------------------------- | ------------------------------------------------------------------------------------------------ |
| `schedule` | `string`                     | 一个 [cron 表达式](#cron-expression-syntax) 或昵称，如 `"@hourly"`。                                        |
| `handler`  | `(this: CronJob) => unknown` | 每次触发时调用。可以返回 Promise — 在其安顿之前不会调度下一次触发。在 `function` 回调内，`this` 是 `CronJob`（因此 `this.stop()` 有效）。 |

同步返回一个 [`CronJob`](#the-cronjob-handle)。如果表达式无效或没有未来出现机会，则抛出 `TypeError`，如 `"0 0 30 2 *"`（2 月 30 日）。

### 无重叠保证

下一次触发时间仅在处理器（包括其返回的任何 `Promise`）安顿后计算。如果您的处理器需要 90 秒，而调度是 `* * * * *`，那么第二次触发是在处理器完成后第一个分钟边界，而不是首次触发后 60 秒。调用永远不会堆叠。

### 错误处理

错误遵循 `setTimeout` 语义：

* 同步 `throw` 会触发 `process.on("uncaughtException")`。
* 被 reject 的返回 `Promise` 会触发 `process.on("unhandledRejection")`。

如果没有监听器，进程以代码 `1` 退出。有监听器时，任务继续运行 — 它不会在第一次失败时停止。

```ts theme={null}
process.on("unhandledRejection", err => log.error("cron failed:", err));

Bun.cron("* * * * *", async () => {
  await mightThrow(); // 记录并下一分钟重试
});
```

### `bun --hot`

在 `bun --hot` 下，所有进程内 cron 任务会在模块图重新评估之前立即停止。源代码中仍然存在的每个 `Bun.cron()` 调用会重新注册。编辑调度、编辑处理器或完全删除该行都会在保存时生效，而不会泄漏定时器。

### `CronJob` 句柄

```ts theme={null}
using job = Bun.cron("0 * * * *", () => {});

job.cron; // => "0 * * * *"
job.stop(); // 取消 — 处理器将不再触发
job.unref(); // 允许进程即使在调度中也能退出
job.ref(); // 保持进程存活（默认）
```

`CronJob` 是 [`Disposable`](https://github.com/tc39/proposal-explicit-resource-management) — `using job = Bun.cron(...)` 在作用域退出时自动停止。`stop()`、`ref()` 和 `unref()` 都返回该任务以支持链式调用。

### 假定时器

进程内 cron 遵循 `jest.useFakeTimers()`。`setSystemTime()`、`advanceTimersByTime()` 和 `runAllTimers()` 控制其触发时间，因此您可以测试计划的回调而无需等待真实时钟。

***

## `Bun.cron(path, schedule, title)` — OS 级别

注册一个按计划运行 JavaScript/TypeScript 模块的 OS 级别 cron 任务。

```ts theme={null}
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");
```

### 参数

| 参数         | 类型       | 说明                    |
| ---------- | -------- | --------------------- |
| `path`     | `string` | 脚本路径（相对于调用者解析）        |
| `schedule` | `string` | Cron 表达式或昵称           |
| `title`    | `string` | 唯一任务标识符（字母数字、连字符、下划线） |

使用相同的 `title` 重新注册会原地覆盖现有任务 — 旧调度被替换，而不是重复。

```ts theme={null}
await Bun.cron("./worker.ts", "0 * * * *", "my-job"); // 每小时
await Bun.cron("./worker.ts", "*/15 * * * *", "my-job"); // 替换：每 15 分钟
```

### `scheduled()` 处理器

注册的脚本必须导出一个默认对象，其中包含 `scheduled()` 方法，遵循 [Cloudflare Workers Cron Triggers API](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/)：

```ts worker.ts theme={null}
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())
  },
};
```

处理器可以是 `async`。Bun 会等待返回的 promise 安顿后才退出。

***

## 各平台的工作原理

### Linux

Bun 使用 [crontab](https://man7.org/linux/man-pages/man5/crontab.5.html) 注册任务。每个任务以一行形式存储在用户的 crontab 中，上方带有 `# bun-cron: <title>` 标记注释。

crontab 条目如下：

```
<schedule> '<bun-path>' run --cron-title=<title> --cron-period='<schedule>' '<script-path>'
```

当 cron 守护进程触发任务时，Bun 会导入您的模块并调用 `scheduled()` 处理器。

**查看已注册的任务：**

```sh theme={null}
crontab -l
```

**日志：** 在 Linux 上，cron 输出发送到系统日志。使用以下命令查看：

```sh theme={null}
# 基于 systemd（Ubuntu、Fedora、Arch 等）
journalctl -u cron       # 或某些发行版上的 crond
journalctl -u cron --since "1 hour ago"

# 基于 syslog（较老系统）
grep CRON /var/log/syslog
```

要将 stdout/stderr 捕获到文件，直接在 crontab 条目中重定向输出，或在 `scheduled()` 处理器中添加日志记录。

**无需代码手动卸载：**

```sh theme={null}
# 编辑 crontab 并删除 "# bun-cron: <title>" 注释
# 及其下方的命令行
crontab -e

# 或一次性删除所有 bun cron 任务：
crontab -l | grep -v "# bun-cron:" | grep -v "\-\-cron-title=" | crontab -
```

### macOS

Bun 使用 [launchd](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html) 注册任务。每个任务作为 plist 文件安装到：

```
~/Library/LaunchAgents/bun.cron.<title>.plist
```

plist 使用 `StartCalendarInterval` 定义调度。支持带有范围、列表或步长的复杂模式 — Bun 会将其展开为多个 `StartCalendarInterval` 字典（笛卡尔积）。

**查看已注册的任务：**

```sh theme={null}
launchctl list | grep bun.cron
```

**日志：** stdout 和 stderr 写入到：

```
/tmp/bun.cron.<title>.stdout.log
/tmp/bun.cron.<title>.stderr.log
```

例如，一个标题为 `weekly-report` 的任务：

```sh theme={null}
cat /tmp/bun.cron.weekly-report.stdout.log
tail -f /tmp/bun.cron.weekly-report.stderr.log
```

**无需代码手动卸载：**

```sh theme={null}
# 从 launchd 卸载任务
launchctl bootout gui/$(id -u)/bun.cron.<title>

# 删除 plist 文件
rm ~/Library/LaunchAgents/bun.cron.<title>.plist

# 标题为 "weekly-report" 的任务示例：
launchctl bootout gui/$(id -u)/bun.cron.weekly-report
rm ~/Library/LaunchAgents/bun.cron.weekly-report.plist
```

### Windows

Bun 使用 [Windows Task Scheduler](https://learn.microsoft.com/zh-cn/windows/win32/taskschd/task-scheduler-start-page) 和基于 XML 的任务定义。每个任务注册为名为 `bun-cron-<title>` 的计划任务，使用 [`CalendarTrigger`](https://learn.microsoft.com/zh-cn/windows/win32/taskschd/taskschedulerschema-calendartrigger-triggergroup-element) 元素和 [`Repetition`](https://learn.microsoft.com/zh-cn/windows/win32/taskschd/taskschedulerschema-repetition-triggerbasetype-element) 模式。

大多数 cron 表达式得到完全支持，包括 `@daily`、`@weekly`、`@monthly`、`@yearly`、范围（`1-5`）、列表（`1,15`）、命名日期/月份和日期模式。

#### 用户上下文

Bun 使用 [`S4U`（Service-for-User）](https://learn.microsoft.com/zh-cn/windows/win32/taskschd/taskschedulerschema-logontype-simpletype) 登录类型注册任务，即使用户未登录也会以注册用户身份运行任务 — 与 Linux `crontab` 行为一致。不存储密码。

TCP/IP 网络（`fetch()`、HTTP、WebSocket、数据库连接）正常工作。唯一的限制是 S4U 任务无法访问 [Windows 认证的网络资源](https://learn.microsoft.com/zh-cn/windows/win32/taskschd/security-contexts-for-running-tasks)（SMB 文件共享、映射驱动器、Kerberos/NTLM 服务）。

在无头服务器和 CI 环境中，如果当前用户的[安全标识符（SID）](https://learn.microsoft.com/zh-cn/windows/security/identity-protection/access-control/security-identifiers)无法解析 — 例如由 [NSSM](https://nssm.cc/) 或类似工具创建的服务帐户 — `Bun.cron()` 会失败并显示说明问题的错误。解决方法是：以普通用户帐户运行 Bun，或使用 `schtasks /create /xml <file> /tn <name> /ru SYSTEM /f` 手动创建计划任务。

#### 触发器限制

<Warning>
  Windows Task Scheduler 强制执行每个任务 [48 个触发器](https://learn.microsoft.com/zh-cn/windows/win32/taskschd/taskschedulerschema-triggers-tasktype-element)的限制（`CalendarTrigger` 元素具有 [`maxOccurs="48"`](https://learn.microsoft.com/zh-cn/windows/win32/taskschd/taskschedulerschema-calendartrigger-triggergroup-element)）。一些在 Linux 和 macOS 上有效的 cron 表达式在 Windows 上会超过此限制。当模式超过限制时，`Bun.cron()` 会以错误消息 reject。
</Warning>

**在所有平台上都有效的表达式：**

| 模式                                      | 触发器策略                                                                                                                                         | 计数 |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -- |
| `*/5 * * * *`                           | 单个触发器配合 [`Repetition`](https://learn.microsoft.com/zh-cn/windows/win32/taskschd/taskschedulerschema-repetition-triggerbasetype-element)（PT5M） | 1  |
| `*/15 * * * *`                          | 单个触发器配合 Repetition（PT15M）                                                                                                                     | 1  |
| `0 9 * * MON-FRI`                       | 每个工作日一个 `CalendarTrigger`                                                                                                                     | 5  |
| `0,30 9-17 * * *`                       | 2 分钟 × 9 小时                                                                                                                                   | 18 |
| `@daily`、`@weekly`、`@monthly`、`@yearly` | 单个触发器                                                                                                                                         | 1  |

**在 Windows 上失败的表达式**（但在 Linux 和 macOS 上有效）：

| 模式                | 原因                        | 触发器计数 |
| ----------------- | ------------------------- | ----- |
| `*/7 * * * *`     | 9 个分钟值 × 24 小时            | 216   |
| `*/8 * * * *`     | 8 个分钟值 × 24 小时            | 192   |
| `*/9 * * * *`     | 7 个分钟值 × 24 小时            | 168   |
| `*/11 * * * *`    | 6 个分钟值 × 24 小时            | 144   |
| `*/13 * * * *`    | 5 个分钟值 × 24 小时            | 120   |
| `*/15 * * 6 *`    | 月份限制阻止了 Repetition：4 × 24 | 96    |
| `0,30 * 15 * FRI` | OR 拆分使触发器翻倍：2 × 24 × 2    | 96    |

关键因素是表达式是否可以使用 [`Repetition`](https://learn.microsoft.com/zh-cn/windows/win32/taskschd/taskschedulerschema-repetition-triggerbasetype-element) 间隔（单个触发器）还是必须展开为单独的 `CalendarTrigger` 元素。**能整除 60** 的分钟步长（`*/1`、`*/2`、`*/3`、`*/4`、`*/5`、`*/6`、`*/10`、`*/12`、`*/15`、`*/20`、`*/30`）使用 Repetition，无论其他字段如何都能工作。不能整除 60 的步长（`*/7`、`*/8`、`*/9`、`*/11`、`*/13` 等）必须展开，而在 24 小时活跃的情况下，计数很快就会超过 48。

要解决此问题，请简化表达式或限制小时范围：

```ts theme={null}
// ❌ 在 Windows 上失败：*/7 所有小时 = 216 个触发器
await Bun.cron("./job.ts", "*/7 * * * *", "my-job");

// ✅ 有效：限制到特定小时（9 个值 × 5 小时 = 45 个触发器）
await Bun.cron("./job.ts", "*/7 9-13 * * *", "my-job");

// ✅ 有效：改用 60 的约数（Repetition，1 个触发器）
await Bun.cron("./job.ts", "*/5 * * * *", "my-job");
```

#### Windows 容器

<Warning>
  `Bun.cron()` 在 Windows Docker 容器中不受支持。Task Scheduler 服务在 `servercore` 或 `nanoserver` 镜像中未运行。对于容器化工作负载，请使用进程内调度器。
</Warning>

**查看已注册的任务：**

```powershell theme={null}
schtasks /query /tn "bun-cron-<title>"

# 列出所有 bun cron 任务
schtasks /query | findstr "bun-cron-"
```

**无需代码手动卸载：**

```powershell theme={null}
schtasks /delete /tn "bun-cron-<title>" /f

# 示例：
schtasks /delete /tn "bun-cron-weekly-report" /f
```

或打开**任务计划程序**（taskschd.msc），找到名为 `bun-cron-<title>` 的任务，右键单击并删除。

***

## `Bun.cron.remove()`

按标题移除先前注册的 cron 任务。适用于所有平台。

```ts theme={null}
await Bun.cron.remove("weekly-report");
```

这会逆转 `Bun.cron()` 所做的操作：

| 平台      | `remove()` 的作用                      |
| ------- | ----------------------------------- |
| Linux   | 编辑 crontab 以移除条目及其标记注释              |
| macOS   | 运行 `launchctl bootout` 并删除 plist 文件 |
| Windows | 运行 `schtasks /delete` 以移除计划任务       |

移除不存在的任务会无错误地 resolve。
