Bun 内置支持注册操作系统级别的 cron 任务以及解析 cron 表达式。
快速开始
解析 cron 表达式以查找下一次匹配的时间:
// 下一个工作日上午 9:30 UTC
const next = Bun.cron.parse("30 9 * * MON-FRI");
console.log(next); // => 2025-01-20T09:30:00.000Z
注册一个按计划运行脚本的 cron 任务:
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");
Bun.cron.parse()
解析 cron 表达式并返回下一个匹配的 UTC Date。
const next = Bun.cron.parse("*/15 * * * *");
console.log(next); // => 下一个一刻钟边界
| 参数 | 类型 | 描述 |
|---|
expression | string | 5 字段的 cron 表达式或预定义别名 |
relativeDate | Date | number | 搜索的起始点(默认为 Date.now()) |
返回值
Date | null — 下一个匹配的 UTC 时间,如果在约 4 年内没有匹配(例如 2 月 30 日)则返回 null。
链式调用
重复调用 parse() 以获取即将到来的时间序列:
const from = Date.UTC(2025, 0, 15, 10, 0, 0);
const first = Bun.cron.parse("0 * * * *", from);
console.log(first); // => 2025-01-15T11:00:00.000Z
const second = Bun.cron.parse("0 * * * *", first);
console.log(second); // => 2025-01-15T12:00:00.000Z
Cron 表达式语法
标准 5 字段格式:minute hour day-of-month month day-of-week
| 字段 | 取值 | 特殊字符 |
|---|
| Minute | 0–59 | * , - / |
| Hour | 0–23 | * , - / |
| Day of month | 1–31 | * , - / |
| Month | 1–12 或 JAN–DEC | * , - / |
| Day of week | 0–7 或 SUN–SAT | * , - / |
特殊字符
| 字符 | 说明 | 示例 |
|---|
* | 所有值 | * * * * * — 每分钟 |
, | 列表 | 1,15 * * * * — 第 1 和第 15 分钟 |
- | 范围 | 9-17 * * * * — 第 9 到 17 分钟 |
/ | 步长 | */15 * * * * — 每 15 分钟 |
命名值
月份和星期字段接受不区分大小写的名称:
// 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 * * * * | 每小时一次 |
const next = Bun.cron.parse("@daily");
console.log(next); // => 下一个午夜 UTC
日期和星期字段的交互
当同时指定了 day-of-month 和 day-of-week(两者都不是 *)时,只要任一条件为真,表达式就会匹配。这遵循 POSIX cron 标准。
// 在每月 15 日或每周五触发
Bun.cron.parse("0 0 15 * FRI");
当只指定了一个(另一个是 *)时,仅使用该字段进行匹配。
Bun.cron()
注册一个操作系统级别的 cron 任务,按计划运行 JavaScript/TypeScript 模块。
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");
| 参数 | 类型 | 描述 |
|---|
path | string | 脚本路径(相对于调用者解析) |
schedule | string | Cron 表达式或别名 |
title | string | 唯一任务标识符(字母数字、连字符、下划线) |
使用相同的 title 重新注册会就地覆盖现有任务 —— 旧的计划被替换,而非重复添加。
await Bun.cron("./worker.ts", "0 * * * *", "my-job"); // 每小时
await Bun.cron("./worker.ts", "*/15 * * * *", "my-job"); // 替换为:每 15 分钟
scheduled() 处理程序
注册的脚本必须导出一个带有 scheduled() 方法的默认对象,遵循 Cloudflare Workers Cron Triggers API:
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 注册任务。每个任务作为一行存储在您用户的 crontab 中,上方带有 # bun-cron: <title> 标记注释。
crontab 条目如下所示:
<schedule> '<bun-path>' run --cron-title=<title> --cron-period='<schedule>' '<script-path>'
当 cron 守护进程触发任务时,Bun 会导入您的模块并调用 scheduled() 处理程序。
查看已注册的任务:
日志: 在 Linux 上,cron 输出进入系统日志。使用以下命令查看:
# 基于 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() 处理程序内添加日志记录。
无需代码手动卸载:
# 编辑您的 crontab 并删除 "# bun-cron: <title>" 注释
# 及其下方的命令行
crontab -e
# 或通过过滤一次性删除所有 bun cron 任务:
crontab -l | grep -v "# bun-cron:" | grep -v "\-\-cron-title=" | crontab -
macOS
Bun 使用 launchd 注册任务。每个任务作为 plist 文件安装在:
~/Library/LaunchAgents/bun.cron.<title>.plist
plist 使用 StartCalendarInterval 定义计划。支持包含范围、列表或步长的复杂模式 —— Bun 通过笛卡尔积将它们展开为多个 StartCalendarInterval 字典。
查看已注册的任务:
launchctl list | grep bun.cron
日志: stdout 和 stderr 写入到:
/tmp/bun.cron.<title>.stdout.log
/tmp/bun.cron.<title>.stderr.log
例如,一个标题为 weekly-report 的任务:
cat /tmp/bun.cron.weekly-report.stdout.log
tail -f /tmp/bun.cron.weekly-report.stderr.log
无需代码手动卸载:
# 从 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 使用基于 XML 任务定义的 Windows Task Scheduler,使用 CalendarTrigger 元素和 Repetition 模式。
大多数 cron 表达式都得到完全支持,包括 @daily、@weekly、@monthly、@yearly、范围 (1-5)、列表 (1,15)、命名日/月和日期模式。
用户上下文
任务使用 S4U (Service-for-User) 登录类型注册,即使用户未登录也会以注册用户的身份运行任务 —— 与 Linux crontab 行为一致。不存储密码。
TCP/IP 网络(fetch()、HTTP、WebSocket、数据库连接)正常工作。唯一的限制是 S4U 任务无法访问 Windows 认证的网络资源(SMB 文件共享、映射驱动器、Kerberos/NTLM 服务)。
在无法解析当前用户 Security Identifier (SID) 的无头服务器和 CI 环境中 —— 例如由 NSSM 或类似工具创建的服务账户 —— Bun.cron() 将失败并返回解释该问题的错误。要解决此问题,可以将 Bun 作为普通用户账户运行,或使用 schtasks /create /xml <file> /tn <name> /ru SYSTEM /f 手动创建计划任务。
触发器限制
Windows Task Scheduler 对每个任务的触发器数量限制为 48 个(CalendarTrigger 元素的 maxOccurs="48")。
某些在 Linux 和 macOS 上有效的 cron 表达式在 Windows 上会超出此限制。当模式超出限制时,Bun.cron() 将返回错误消息拒绝该表达式。
在所有平台上都有效的表达式:
| 模式 | 触发器策略 | 数量 |
|---|
*/5 * * * * | 使用 Repetition (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 间隔(单一触发器),或者必须展开为独立的 CalendarTrigger 元素。能整除 60的分钟步长(*/1、*/2、*/3、*/4、*/5、*/6、*/10、*/12、*/15、*/20、*/30)使用 Repetition,无论其他字段如何都能工作。不能整除 60 的步长(*/7、*/8、*/9、*/11、*/13 等)必须展开,而在 24 小时全天运行的情况下,数量很快会超过 48。
要解决此问题,请简化表达式或限制小时范围:
// ❌ 在 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 容器
Bun.cron() 在 Windows Docker 容器中不受支持。Task Scheduler 服务不在 servercore 或 nanoserver 镜像中运行。对于容器化工作负载,请使用进程内调度器。
查看已注册的任务:
schtasks /query /tn "bun-cron-<title>"
# 列出所有 bun cron 任务
schtasks /query | findstr "bun-cron-"
无需代码手动卸载:
schtasks /delete /tn "bun-cron-<title>" /f
# 示例:
schtasks /delete /tn "bun-cron-weekly-report" /f
或者打开任务计划程序(taskschd.msc),找到名为 bun-cron-<title> 的任务,右键单击并删除它。
Bun.cron.remove()
通过其标题移除先前注册的定时任务。适用于所有平台。
await Bun.cron.remove("weekly-report");
这会撤销 Bun.cron() 的操作:
| 平台 | remove() 的操作 |
|---|
| Linux | 编辑 crontab 以移除该条目及其标记注释 |
| macOS | 运行 launchctl bootout 并删除 plist 文件 |
| Windows | 运行 schtasks /delete 以移除计划任务 |
移除一个不存在的任务会无错误地完成。