运行时包管理器打包器测试运行器代码检查格式化指南参考博客反馈

Cron

使用 Bun 调度和解析 cron 任务

Bun 内置了对 cron 的支持——解析表达式,在进程内部按计划运行回调,或注册能够跨重启存活的操作系统级任务。

快速开始

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

TypeScript
Bun.cron("0 * * * *", async () => {
  await cleanupTempFiles();
});

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

TypeScript
// 下一个工作日当地时间上午 9:30
const next = Bun.cron.parse("30 9 * * MON-FRI");

注册一个操作系统级 cron 任务,在指定计划下运行脚本:

TypeScript
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");

Bun.cron.parse()

解析 cron 表达式,并返回系统本地时区中下一个匹配的 Date。

TypeScript
const next = Bun.cron.parse("*/15 * * * *");
console.log(next); // => 下一个一刻钟边界

参数

参数类型描述
expressionstring5 字段 cron 表达式或预定义别名
relativeDateDate | number搜索的起始点(默认为 Date.now())
options{ tz?: string }用于解释计划的 IANA 时区名称(默认为系统时区)

返回值

Date | null — 下一个匹配的时间;如果在 8 年内不存在匹配项,则返回 null(例如 2 月 30 日)。

链式调用

重复调用 parse() 以获取即将到来的时间序列:

TypeScript
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 分钟

命名值

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

TypeScript
// 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 / @annually0 0 1 1 *每年一次(1 月 1 日)
@monthly0 0 1 * *每月一次(每月 1 日)
@weekly0 0 * * 0每周一次(周日)
@daily / @midnight0 0 * * *每天一次(午夜)
@hourly0 * * * *每小时一次
TypeScript
const next = Bun.cron.parse("@daily");
console.log(next); // => 下一个本地午夜

时区

Bun 会按照系统的本地时区解释计划,方式与 crontab、launchd 和 Windows Task Scheduler 读取计划的方式相同。操作系统级形式和进程内回调形式会在同一墙上时钟时间触发。

如需覆盖默认时区,请将 IANA 时区名称作为 { tz } 传递给 Bun.cron.parse() 或进程内的 Bun.cron(schedule, handler, options):

TypeScript
// 无论服务器的时区是什么,均为 09:00 UTC
Bun.cron.parse("0 9 * * *", Date.now(), { tz: "UTC" });

// 在纽约时间 09:00 触发
Bun.cron("0 9 * * *", handler, { tz: "America/New_York" });

夏令时转换:

  • 春季调快 — 如果计划任务落在缺失的小时内,则会在当天触发,并因时间间隔而顺延(例如,30 2 * * * 会在春季调快当天的 3:30 运行)。对于缺失小时内的多分钟模式(*/15 2 * * *),只有第一个匹配项会触发。
  • 秋季调慢 — 在重复小时内的固定时间计划(30 1 * * *)只会触发一次,即在第一次出现时触发。分钟或小时字段为 * 的计划(0 * * * *、* * * * *)会经过两次出现的时间——每个实际时间分钟触发一次,与 Linux 上的 crontab 行为一致。

月份中的日期与星期交互

当同时指定月中的日期和星期几(两者都不是 *)时,只要任一条件成立,表达式就会匹配。这遵循 POSIX cron 标准。

TypeScript
// 在每月 15 日或每周五触发
Bun.cron.parse("0 0 15 * FRI");

如果只指定其中一个字段(另一个为 *),Bun 只使用指定的字段进行匹配。


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

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

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

进程内调度是面向长时间运行的服务器和工作进程的轻量级选项。它不需要系统 cron 守护进程,在每个平台上的工作方式都相同,并且调用之间共享状态(数据库连接池、缓存、模块级变量)。

进程内操作系统级
进程退出/重启后是否保留否是
多次运行之间是否共享状态是否(每次都是全新进程)
平台要求无crontab / launchd / Task Scheduler
Windows 表达式限制无48 个触发器上限
返回类型CronJobPromise<void>

参数

参数类型描述
schedulestringcron 表达式,或 "@hourly" 这类昵称。
handler(this: CronJob) => unknown每次触发时调用。可以返回 Promise;Bun 会等它完成后才安排下一次触发。在 function 回调内部,this 是 CronJob(因此 this.stop() 可以正常工作)。
options{ tz?: string }用于解释计划的 IANA 时区名称(默认为系统时区)。

同步返回一个 CronJob。如果表达式无效、时区名称未知,或者表达式没有未来的发生时间(例如 "0 0 30 2 *",即 2 月 30 日),则抛出 TypeError。

不重叠保证

Bun 仅在处理程序及其返回的 Promise 完成后才计算下一次触发时间。如果你的处理程序需要 90 秒,而计划是 * * * * *,那么第二次触发会在处理程序完成后的第一个分钟边界发生,而不是在第一次触发后 60 秒发生。调用不会堆叠。

错误处理

错误行为与 setTimeout 保持一致:

  • 同步 throw 会触发 process.on("uncaughtException")。
  • 被拒绝的返回 Promise 会触发 process.on("unhandledRejection")。

没有监听器时,进程会以代码 1 退出。设置了监听器时,任务会继续运行——不会在第一次失败后停止。

TypeScript
process.on("unhandledRejection", err => log.error("cron failed:", err));

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

bun --hot

在 bun --hot 模式下,Bun 会在模块图重新求值之前立即停止所有进程内 cron 任务。随后,源码中仍存在的每个 Bun.cron() 调用都会重新注册。无论是编辑计划、编辑处理程序,还是完全删除该行,保存后都会立即生效,且不会泄漏定时器。

CronJob 句柄

TypeScript
using job = Bun.cron("0 * * * *", () => {});

job.cron; // => "0 * * * *"
job.stop(); // 取消 — handler 将不再触发
job.unref(); // 即使仍处于计划状态,也允许进程退出
job.ref(); // 保持进程运行(默认)

CronJob 是 Disposable —— using job = Bun.cron(...) 会在作用域退出时自动停止。stop()、ref() 和 unref() 都会返回该 job 以便链式调用。

假定时器

进程内 cron 支持 jest.useFakeTimers()。setSystemTime()、advanceTimersByTime() 和 runAllTimers() 控制其触发时间,因此你可以无需等待真实时钟即可测试计划回调。


Bun.cron(path, schedule, title) — 操作系统级别

注册一个操作系统级别的 cron 任务,按计划运行 JavaScript/TypeScript 模块。

TypeScript
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");

参数

参数类型描述
pathstring脚本路径(相对于调用者解析)
schedulestringCron 表达式或别名
titlestring唯一任务标识符(字母数字、连字符、下划线)

使用相同的 title 重新注册会就地覆盖现有任务。Bun 会替换旧计划,而不是创建重复任务。

TypeScript
await Bun.cron("./worker.ts", "0 * * * *", "my-job"); // 每小时
await Bun.cron("./worker.ts", "*/15 * * * *", "my-job"); // 替换为:每 15 分钟

scheduled() 处理程序

注册的脚本必须导出一个带有 scheduled() 方法的默认对象,遵循 Cloudflare Workers Cron Triggers API:

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())
  },
};

处理程序可以是 async。Bun 会等待返回的 promise 完成后再退出。


各平台的工作原理

Linux

Bun 使用 crontab 注册任务。Bun 会将每个任务作为一行写入用户的 crontab,并在其上方添加 # bun-cron: <title> 标记注释。

crontab 条目如下所示:

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

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

查看已注册的任务:

shell
crontab -l

日志: 在 Linux 上,cron 输出进入系统日志。使用以下命令查看:

shell
# 基于 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() 处理程序内添加日志记录。

无需代码手动卸载:

shell
# 编辑您的 crontab 并删除 "# bun-cron: <title>" 注释
# 及其下方的命令行
crontab -e

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

macOS

Bun 使用 launchd 注册任务。Bun 会将每个任务安装为 plist 文件,路径为:

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

plist 使用 StartCalendarInterval 定义计划。支持带范围、列表或步长的复杂模式。Bun 会将它们展开为多个 StartCalendarInterval 字典,形成笛卡尔积。

查看已注册的任务:

shell
launchctl list | grep bun.cron

日志: stdout 和 stderr 写入到:

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

例如,一个标题为 weekly-report 的任务:

shell
cat /tmp/bun.cron.weekly-report.stdout.log
tail -f /tmp/bun.cron.weekly-report.stderr.log

无需代码手动卸载:

shell
# 从 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 和基于 XML 的任务定义。Bun 会将每个任务注册为名为 bun-cron-<title> 的计划任务,并使用 CalendarTrigger 元素和 Repetition 模式。

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

用户上下文

Bun 使用 S4U(Service-for-User)登录类型注册任务,即使用户未登录,任务也会以注册用户的身份运行。Linux crontab 的行为相同。不会存储密码。

TCP/IP 网络(fetch()、HTTP、WebSocket、数据库连接)正常工作。唯一的限制是 S4U 任务无法访问使用 Windows 身份验证的网络资源(SMB 文件共享、映射驱动器、Kerberos/NTLM 服务)。

在某些无头服务器和 CI 环境中,无法解析当前用户的安全标识符(SID),例如由 NSSM 或类似工具创建的服务帐户。在这种情况下,Bun.cron() 会失败并显示说明问题的错误。要解决此问题,可以将 Bun 作为普通用户帐户运行,或者使用 schtasks /create /xml <file> /tn <name> /ru SYSTEM /f 手动创建计划任务。

触发器限制

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

模式触发器策略数量
*/5 * * * *使用 Repetition(PT5M)的单个触发器1
*/15 * * * *使用 Repetition(PT15M)的单个触发器1
0 9 * * MON-FRI一个列出全部五个工作日的 CalendarTrigger1
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 × 2496
0,30 * 15 * FRIOR 分割使触发器翻倍:2 × 24 × 296

关键因素在于表达式能否使用 Repetition 间隔(单个触发器),还是必须展开为单独的 CalendarTrigger 元素。能够整除 60 的分钟步长(*/1、*/2、*/3、*/4、*/5、*/6、*/10、*/12、*/15、*/20、*/30)只有在其他四个字段均未受限制(*)时才会仅使用 Repetition。不能整除 60 的步长(*/7、*/8、*/9、*/11、*/13 等)必须展开;当 24 小时都处于启用状态时,触发器数量会迅速超过 48 个。

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

TypeScript
// ❌ 在 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 容器

查看已注册的任务:

PowerShell
schtasks /query /tn "bun-cron-<title>"

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

无需代码手动卸载:

PowerShell
schtasks /delete /tn "bun-cron-<title>" /f

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

或者打开任务计划程序(taskschd.msc),找到名为 bun-cron-<title> 的任务,右键单击并删除它。


Bun.cron.remove()

通过其标题移除先前注册的定时任务。适用于所有平台。

TypeScript
await Bun.cron.remove("weekly-report");

这会撤销 Bun.cron() 的操作:

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

移除一个不存在的任务会无错误地完成。