Cron Triggers 是 Workers 内置的定时执行机制:在 wrangler 配置里写一段 cron 表达式,Cloudflare 按时间调用你 Worker 里的 scheduled() 处理器,VPS 上的 crontab 能干的事它大多能干,还不用维护一台常开的服务器。第一个要记住的事实:它按 UTC 时间执行,表达式 0 1 * * * 对应北京时间 09:00,不是凌晨一点。 2026 年 10 月,我把三个原来跑在 VPS 上的小定时任务(每日备份检查、Token 配额统计、缓存预热)迁到 Workers Cron,实测记录如下。
上一篇给 Workers 配了 D1 数据库,这篇让 Worker 定时干活。三个任务里最疼的不是迁移,是时区和重试:一个表达式差 8 小时,一次外部接口失败被重试三次扣了三份额度。

Cron Triggers 适合干哪些活?
结论:周期性、无状态、分钟级粒度的活都合适,需要秒级精度或长执行时间的不要用。
| 场景 | 合适吗 | 说明 |
|---|---|---|
| 每日拉取数据写 KV/D1 | 合适 | 典型用法,边缘直接落库 |
| 定期清理过期数据 | 合适 | 配合 D1 删除语句或 KV TTL |
| 每小时巡检接口发告警 | 合适 | 配合外部通知 API |
| 秒级调度 | 不合适 | 最小粒度是分钟 |
| 单次跑几分钟的批处理 | 不合适 | Workers 单次 CPU 时间有限制 |
官方文档在 Cron Triggers,写明 Cron 按 UTC 执行、支持五个字段的 cron 表达式外加 L、W 这类 Quartz 扩展。执行顺序上,Cloudflare 把定时任务调度到空闲机器上跑,这意味着同一个任务两次执行的机器可能不同,别在 scheduled() 里依赖本地状态。

第一步:写 scheduled 处理器
结论:Worker 里加一个 scheduled(controller, env, ctx) 导出,fetch 和 scheduled 可以共存,同一个 Worker 白天接请求夜里跑任务。
export default {
async fetch(request, env) {
return new Response("ok");
},
async scheduled(controller, env, ctx) {
// 按 controller.cron 区分不同表达式的任务
if (controller.cron === "0 1 * * *") {
await dailyBackupCheck(env);
} else if (controller.cron === "*/30 * * * *") {
await quotaStats(env);
}
},
};
一个 Worker 可以挂多个 cron 表达式,用 controller.cron 判断这次是哪个表达式触发的。官方文档用这个字段做任务分发,我沿用。ctx.waitUntil() 可以挂异步任务让运行时等它完成,别忘了用它包住慢操作,否则执行器不等待结果。

第二步:配置表达式,先过一遍时区换算
结论:表达式按 UTC,北京时间减 8。想要北京时间 09:00 跑,写 0 1 * * ;想要每天 0 点跑,写 0 16 * * 。
wrangler.toml:
[triggers]
crons = [ "0 1 * * *", "*/30 * * * *" ]
换算表(收藏用):
| 想要的北京时间 | UTC 表达式 |
|---|---|
| 每天 09:00 | 0 1 * * * |
| 每天 00:00 | 0 16 * * * |
| 每小时 | 0 * * * * |
| 每 30 分钟 | */30 * * * * |
| 工作日 18:00 | 0 10 * * 1-5 |
我的第一个任务要的是「每天早上九点检查备份」,照习惯写了 0 9 * * *,第二天下午五点收到通知。文档里 UTC 这句话就在开头第二段,但我抄表达式时没看,这篇把它放在最前面。

第三步:本地测试,别靠等线上触发
结论:wrangler dev --test-scheduled 会开一个 /cdn-cgi/local/scheduled 端点,curl 一下就触发,还能用 ?cron= 参数模拟任意表达式。
$ npx wrangler dev --test-scheduled
$ curl "http://localhost:8787/cdn-cgi/local/scheduled"
$ curl "http://localhost:8787/cdn-cgi/local/scheduled?cron=0+1+*+*+*"
这个端点省掉了「部署完等一个小时看结果」的循环。测试完部署 npx wrangler deploy,线上配置就带上了触发器。

第四步:失败重试,外部接口的额度扣了三份
结论:scheduled() 抛异常时 Cloudflare 默认会重试,对有副作用的外部调用(扣费接口、发通知)要用 controller.noRetry() 明确关掉,或者把幂等做进任务里。
我的 Token 配额统计任务调了一个计费 API,那天该 API 超时,Worker 抛错后 Cloudflare 自动重试,同一个统计被记了三次。修法分两档:
async scheduled(controller, env, ctx) {
try {
await quotaStats(env);
} catch (err) {
console.error("quota failed", err);
controller.noRetry(); // 失败也不重试,等下一轮 cron
}
}
另一档是幂等设计:统计结果写入前先带日期 key 查重,当天已写入就跳过。重试机制本身是好事,瞬时故障自动恢复,要防的只是「重试导致副作用放大」。官方文档说明触发器配置的增删改最长 15 分钟才传播到全网,改完别急着看效果,这也是我一开始误判「没生效」的原因。

第五步:线上排错,看 Cron Events
结论:Dashboard 里 Worker 的 Settings → Trigger Events 存最近 100 次定时调用的记录,成功失败一目了然,配合 Workers Logs 看执行日志。
我排那次重复统计,就是在 Cron Events 里看到同一分钟三条失败记录,重试机制才实锤。用 Wrangler 的话,npx wrangler tail 实时看日志流,也能抓到 scheduled 触发。

迁移 VPS crontab 的取舍
结论:无状态小任务迁 Workers 省一台常驻服务的心智,重 IO 和长任务留在 VPS,两边用同一个通知渠道归口。
三个任务迁了两个。剩下的那个是数据库 nightly 备份(mysqldump 打包上传),执行要四分钟、依赖本地磁盘,留在 VPS 上用 systemd timer 管,理由写在 timer 那篇里。判断标准就两条:执行时长和磁盘依赖。Workers 拿不出持久磁盘,CPU 时间也有限制,超了就别硬塞。
迁移还有一笔隐性收益值得记:Workers 侧的任务天然带着执行日志和重试语义,原来 crontab 里「静默失败无人知」的两个任务,迁移后每天在 Cron Events 里留下成功记录,失败还会主动重试。定时任务的可观测性从零到有,这一项比省下的服务器资源更值。
和 D1/KV 组合时,额度账要提前算
结论:Cron 任务的每次执行都计入 Workers 请求数,任务里再读写 D1/KV 则吃各自配额,频率定高了一个月免费额度可能被一个巡检任务吃掉一半。
迁第三个任务前我算过一笔账:每 5 分钟巡检一次接口,一天 288 次调用,加上每次巡检写一条 D1 记录(288 行写)。Workers 免费档每天 10 万请求、D1 每天免费写 10 万行,单任务看都富余,但同一台账号上还挂着短链、评论这些常驻 Worker,额度是账号级共享的。最后把巡检频率从 5 分钟放宽到 15 分钟,一天 96 次,账面立刻健康——定时任务的频率该由「出问题多快需要知道」倒推,不是越高越负责。
给要做同类组合的人一个参考:把每个任务的「次数 × 读写行数」列进一张表再定 cron 表达式,五分钟的事,省掉月底额度报警的半夜处理。
常见问题
Q:Cron Trigger 收费吗?
A:免费档可用,限制在每日请求数额度内,定时调用和普通请求一样计数。个人项目量级完全够。
Q:能不能精确到秒?
A:不能,最小粒度分钟。需要秒级调度用外部调度器打 HTTP 端点。
Q:scheduled 执行多久算超时?
A:受 Workers 的执行限制约束,CPU 时间和挂钟时间各有上限,配合 waitUntil 的总时长也有约束。备份这类长 IO 任务别放 Workers。
Q:怎么确认线上任务真的在跑?
A:三个手段叠加:任务里写一条 KV 或 D1 记录(带时间戳),Cron Events 看调用历史,外部通知渠道每天报一次平安。我三个都用了,出问题时交叉定位快。
还有一个实战细节:多个 cron 任务共写一张状态表时,给每条记录带上任务名和执行时间戳做 key,既当幂等锁又当运行日志,Cron Events 看不到的历史全在这张表里。定时任务的可观测性靠的不是平台功能,是你自己留的那一行记录。