bash
// 7 月 7 日 0 点整,跑一次
Bun.cron("0 0 7 7 *", () => {
console.log("7 月 7 日到了,Bun 1.4 rust 发布了");
});
bash
bun run job.ts
完事儿。
进程不退出,7 月 7 日凌晨 0 点 0 分,handler 自动触发。
为啥是7月7日?

⭐bun1.4 也就是rust版本就要发布了, 真的很期待, 等到时候我会写一篇1.4介绍的文章。
根据官方x上的介绍, 已知变化有:
-
底层换 Rust
-
React 编译快 19 倍
-
SourceMap 解码快 3 倍
-
hex 编码快 4.8 倍
-
base64url 快 41 倍
-
CJK 宽度计算快 56 倍
-
fetch 自动压缩请求体
-
内存不足自动预警
痛点:写个定时任务有多折腾
老办法写一遍:
bash
# 装包
npm i node-cron
bash
import cron from "node-cron";
cron.schedule("0 0 7 7 *", () => {
sendCoupon();
});
先别说 node-cron 五年没更新了。
就说跨平台这事儿。
Linux 要写 crontab。macOS 要写 launchd。Windows 要写 Task Scheduler。
三套配置。
三个调试姿势。
部署上线,crontab 没装,任务根本起不来。
Bun.cron 就是来治这个的。
v1.3.11 内置,啥都不用装。
进程内 Bun.cron()、系统级 Bun.cron(path, schedule, title) 一把梭。
一、进程内定时:3 行起步
Bun.cron(schedule, handler) 在当前进程里跑。
handler 可以是 async。
下一个触发时间,等 handler 跑完再算。
不会堆叠。
bash
// 每天 9 点整
Bun.cron("0 9 * * *", async () => {
await syncOrders();
});
// 每周一到周五,下午 6 点
Bun.cron("0 18 * * MON-FRI", async () => {
await generateDailyReport();
});
// 7 月 7 日 0 点 0 分,给老用户发券
Bun.cron("0 0 7 7 *", async () => {
await sendCouponsToVIPs();
});
返回值是个 handle。
bash
const job = Bun.cron("0 0 7 7 *", () => {});
job.cron; // "0 0 7 7 *"
job.stop(); // 取消
job.unref(); // 进程不再被它拖住
job.ref(); // 恢复
还能用 using 自动停:
bash
using job = Bun.cron("0 0 7 7 *", () => {});
// 作用域结束自动 stop
二、cron 表达式速查
Bun.cron 用标准 5 字段:分 时 日 月 周。
| 字段 | 取值 | | :-- | :-- | | 分 | 0-59 | | 时 | 0-23 | | 日 | 1-31 | | 月 | 1-12(也支持 JAN-DEC) | | 周 | 0-7(0 和 7 都是周日,支持 SUN-SAT) |
四个常用特殊字符:
| 符号 | 含义 | 例子 | | :-- | :-- | :-- | | * | 任意 | * * * * * 每分钟 | | , | 列举 | 1,15 * * * * 第 1、第 15 分 | | - | 区间 | 9-17 * * * * 9 到 17 分 | | / | 步长 | */15 * * * * 每 15 分钟 |
几个常用组合:
bash
// 7 月 7 日 0 点 0 分
Bun.cron("0 0 7 7 *", handler);
// 7 月 7 日 12 点 0 分
Bun.cron("0 12 7 7 *", handler);
// 7 月 7 日 18 点 30 分
Bun.cron("30 18 7 7 *", handler);
// 7 月 7 日 18 点 30 分(命名法)
Bun.cron("30 18 7 JUL *", handler);
还有几个内置别名:
bash
"@yearly" // 0 0 1 1 * 1 月 1 日
"@monthly" // 0 0 1 * * 每月 1 号
"@weekly" // 0 0 * * 0 每周日
"@daily" // 0 0 * * * 每天 0 点
"@hourly" // 0 * * * * 每小时
MON-FRI 这种区间名字也支持。
JAN,JUN 多选也行。
完整解析都按 POSIX 标准来。
三、解析下次触发时间:Bun.cron.parse
有时候你不想立刻跑。
就想知道 :下次啥时候跑?
Bun.cron.parse("...") 直接返回 Date 对象。
bash
// 下一次 7 月 7 日 0 点 0 分
const next = Bun.cron.parse("0 0 7 7 *");
console.log(next);
// => 2026-07-07T00:00:00.000Z
// 下一次工作日 9 点半
const workday = Bun.cron.parse("30 9 * * MON-FRI");
console.log(workday);
// 每 15 分钟
const q = Bun.cron.parse("*/15 * * * *");
console.log(q); // 最近的整 15 分
返回 UTC 时间。
没有夏令时问题。
8 年内找不到匹配返回 null(比如 2 月 30 号,永远不会有)。
还能链式查接下来 N 次:
bash
let cursor: Date | number = Date.now();
for (let i = 0; i < 3; i++) {
cursor = Bun.cron.parse("0 * * * *", cursor)!;
console.log(cursor.toLocaleString());
// 输出接下来三个整点
}
写预约系统、倒计时页面、提醒功能的时候特别香。
四、系统级 cron:进程死了也能跑
进程内 cron 跑得欢。
但进程一退出,就停了。
服务器重启、Docker 容器干掉、PM2 reload 一下------任务没了。
想要进程死了也跑?
用系统级 Bun.cron(path, schedule, title):
bash
// 注册一个系统级 cron
// 每 30 分钟跑一次 ./worker.ts
await Bun.cron("./worker.ts", "*/30 * * * *", "heartbeat");
Bun 内部自动调用操作系统的调度器:
| 平台 | 调度器 | | :-- | :-- | | Linux | crontab | | macOS | launchd plist | | Windows | Task Scheduler |
同一个 title 重复注册:
覆盖 旧的,不会重复。
bash
// 这次每 1 小时
await Bun.cron("./worker.ts", "0 * * * *", "heartbeat");
// 改成每 15 分钟
await Bun.cron("./worker.ts", "*/15 * * * *", "heartbeat");
// 旧任务被覆盖,不会跑两个
被调度的脚本必须 export default 一个对象:
bash
// worker.ts
export default {
async scheduled(controller) {
console.log("cron:", controller.cron);
// "0 * * * *"
console.log("时间:", new Date(controller.scheduledTime).toISOString());
await doWork();
},
};
API 跟 Cloudflare Workers 的 Cron Triggers 一模一样。
写过 CF Workers 的兄弟零学习成本。
删除也很简单:
bash
await Bun.cron.remove("heartbeat");
Linux 上看一眼注册了啥:
bash
crontab -l
你会看到 Bun 自动加的标记:
bash
# bun-cron: heartbeat
*/30 * * * * '/usr/local/bin/bun' run --cron-title=heartbeat --cron-period='*/30 * * * *' '/path/worker.ts'
macOS 看一眼:
bash
launchctl list | grep bun.cron
Windows 看一眼:
bash
schtasks /query | findstr "bun-cron-"
五、实战:7 月 7 日 0 点发奖
来个真实业务场景。
7 月 7 日 0 点,给所有 VIP 用户发一张满 100 减 20 的券。
只发一次 ,7 月 8 日之后这个 cron 就没用了。
方案 A:进程内 cron
写一个 long-running 服务:
bash
// job.ts
import { SQL } from "bun";
// 内存里 1 个 cron
Bun.cron("0 0 7 7 *", async () => {
const sql = new SQL("postgres://user:pass@localhost:5432/shop");
// 用 Bun.sql 原生 prepared statement,防注入
const vips = await sql`
SELECT id FROM users WHERE level = 'vip'
`;
for (const user of vips) {
await sql`
INSERT INTO coupons (user_id, amount, expires_at)
VALUES (${user.id}, 20, '2026-07-31')
`;
}
console.log(`已发 ${vips.length} 张券`);
});
bash
bun run job.ts
服务挂着。
7 月 7 日 0 点 0 分自动跑。
7 月 8 日之后 ,这个 cron 永远不会触发(今年 7 月 7 日过完)。
进程也不需要重启。
方案 B:系统级 cron
7 月 7 日 0 点跑一次:
bash
// register.ts
await Bun.cron("./send-coupons.ts", "0 0 7 7 *", "july7-coupon");
console.log("已注册到系统 cron");
bash
// send-coupons.ts
import { SQL } from "bun";
export default {
async scheduled() {
const sql = new SQL("postgres://user:pass@localhost:5432/shop");
const vips = await sql`SELECT id FROM users WHERE level = 'vip'`;
for (const user of vips) {
await sql`
INSERT INTO coupons (user_id, amount, expires_at)
VALUES (${user.id}, 20, '2026-07-31')
`;
}
console.log(`已发 ${vips.length} 张券`);
// 跑完就注销,省得明年 7 月 7 日又跑一次
await Bun.cron.remove("july7-coupon");
},
};
bash
bun run register.ts
7 月 7 日 0 点,操作系统准时拉起 Bun。
跑完自动注销。
明年 7 月 7 日不重复。
六、进程内 vs 系统级,怎么选
| 维度 | 进程内 Bun.cron(schedule, handler) | 系统级 Bun.cron(path, schedule, title) | | :-- | :-- | :-- | | 进程退出 | 停了 | 不停 | | 共享状态 | 有 | 没(每次新进程) | | 跨平台限制 | 无 | Windows 有 48 trigger 上限 | | 返回值 | CronJob handle | Promise<void> | | 适用场景 | 长期服务、worker、API 后台任务 | 一次性任务、运维脚本、数据清理 |
长期挂着不死的服务 (API、Worker),用进程内。
单次、长时间间隔、不能挂服务 (凌晨 3 点备份、跨年发奖),用系统级。
七、几个容易踩的坑
坑 1:UTC 时间
进程内 和 parse() 全部走 UTC。
bash
// 这不是早上 9 点,是 UTC 9 点
Bun.cron("0 9 * * *", handler);
国内 业务想跑北京时间早上 9 点:
bash
// UTC 9 点 = 北京时间 17 点
// 北京时间 9 点 = UTC 1 点
Bun.cron("0 1 * * *", handler);
或者 用 TZ=UTC 起服务,所有时间统一 UTC。
系统级 cron 走系统本地时区(crontab、launchd、Task Scheduler 默认就这样)。
跨平台时 ,两种方式可能差几小时。
坑 2:Windows 48 trigger 上限
Windows Task Scheduler 一个任务最多 48 个触发器。
*/5 * * * * 这种 *+步长的,Bun 会自动转成 Repetition ,1 个 trigger 搞定。
但 */7 * * * * 这种不能整除 60 的 ,Bun 会展开:
bash
// 9 个分 × 24 小时 = 216 trigger ❌ Windows 拒绝
await Bun.cron("./job.ts", "*/7 * * * *", "my-job");
// 改成 9 个分 × 5 小时 = 45 trigger ✅
await Bun.cron("./job.ts", "*/7 9-13 * * *", "my-job");
跨平台部署 的时候,老老实实用 60 的因数 :*/1 */2 */3 */4 */5 */6 */10 */12 */15 */20 */30。
坑 3:Windows 容器不支持
Windows Docker 容器 (servercore、nanoserver)没有 Task Scheduler 服务。
Bun.cron("./job.ts", ...) 在容器里直接报错。
容器场景用进程内 cron。
坑 4:错误处理
handler 抛错 走 setTimeout 语义:
-
• 同步 throw →
process.on("uncaughtException") -
• Promise reject →
process.on("unhandledRejection")
没监听 → 进程退出码 1。
有监听 → 任务继续跑 ,下次到点再试。
bash
process.on("unhandledRejection", (err) => {
log.error("cron 任务挂了:", err);
});
Bun.cron("0 0 7 7 *", async () => {
// 抛错不会让任务停
await mightFail();
});
八、Bun 1.4 rust 版要来了
Bun.cron 现在是 Zig 实现的。
下个大版本,Bun 1.4 ,核心引擎 Zig 换 Rust。
PORTING.md 加 .claude/workflows,AI 流水线迁移。
5 月 14 日 那个 Rewrite Bun in Rust 的 PR 已经 merge。
Linux x64 glibc 99.8% 测试通过。
二进制瘦了 3~8 MB。
性能持平或更快。
想尝鲜:
bash
bun upgrade --canary
进程内 和系统级 cron 在 rust 版里 API 完全不变。
代码不用动。
有数据状态:
-
• v1.3.11 引入
Bun.cron -
• v1.3.14 是 1.3 收官版
-
• 1.4 还在 canary
-
• 7 月 7 日左右出 stable
赶 7 月 7 日发奖:
-
• 业务代码现在 用
Bun.cron写 -
• 上线先用 1.3.x stable
-
• 1.4 出了直接
bun upgrade -
• 代码一行不用改
九、收个尾
Bun.cron 干的就这一件事:
让定时任务少折腾。
-
• 进程内 3 行起步
-
• 系统级一个
await注册 -
• 跨平台一套 API
-
• 表达式支持命名、范围、步长、OR 逻辑
-
• 跟 Cloudflare Workers 一样的
scheduled()handler -
• 错误不打断后续调度
node-cron 五年没更新。
自己写 crontab 维护成本高。
Bun 直接把 cron 焊死在 runtime 里。
7 月 7 日发个券,就这一行:
bash
Bun.cron("0 0 7 7 *", sendCoupons);
完事儿。