Workers 定时任务:cron 触发器与 scheduled 处理函数
读完这一篇,你能给一个 Worker 加上定时任务:在 wrangler.toml 里声明 cron 表达式,写一个 scheduled 处理函数,用 controller.cron 区分不同任务,用 ctx.waitUntil 保证异步收尾不被打断,并且在本地用一条 URL 手动触发它来调试。
本篇没有部署,也没有跑本地验证,配置与代码按文档写法给出。第 5 节的调试方式来自 wrangler dev 的既有能力,命令本身以 wrangler dev --help 的输出为准。
1. 定时任务和 HTTP 请求是两条入口
一个 Worker 可以有多个入口:fetch 处理 HTTP 请求,scheduled 处理定时触发,queue 处理队列消息,tail 处理日志流。它们共用一个 env,但触发方式完全不同。
只做定时任务的 Worker 可以完全没有 fetch:
export default {
async scheduled(controller, env, ctx) {
await doSomething(env);
}
};
这样的 Worker 部署后不能通过 URL 访问,只会在 cron 时间点被调用。反过来说,如果一个 Worker 同时有 fetch 和 scheduled,两者要各写各的,别指望 fetch 里的初始化逻辑对 scheduled 生效——它们是两次独立的执行。
2. 配置 cron 表达式
name = "cf63-cron-demo"
main = "src/index.js"
compatibility_date = "2026-10-08"
[triggers]
crons = ["*/5 * * * *", "0 3 * * *"]
crons 是字符串数组,每条是一个标准 5 段 cron(分、时、日、月、星期)。上面这两条的意思是:每 5 分钟一次,以及每天 03:00 一次。
几个要点:
时间基准是 UTC。写 0 3 * * * 表示 UTC 03:00,对应北京时间 11:00。想在北京时间凌晨 3 点跑,要写 0 19 * * *(前一天 UTC 19:00)。
最小粒度是 1 分钟。* * * * * 是每分钟一次,没有秒级。免费档的 cron 触发器数量有上限,条目多的时候优先合并成一条再在代码里分支。
改完 crons 要重新 wrangler deploy 才生效,改本地文件不会自动同步到线上触发器。
部署输出里会多一段 triggers 信息,列出注册成功的表达式。如果那里是空的,说明 [triggers] 段没被读到——常见原因是 wrangler.toml 里这段被写在了某个 [env.xxx] 下面,或者文件名拼成了 wrangler.json 但内容仍是 TOML 格式。控制台的 Worker 详情页也能看到当前生效的 cron 列表,和配置对一遍最稳。
一个 Worker 挂多条表达式时,scheduled 只会被调用一次,靠 controller.cron 区分是哪一条到点了。把它当作一次普通函数调用来写,不要假设"每次部署只跑一条"。
3. 处理函数的三个参数
export default {
async scheduled(controller, env, ctx) {
const cron = controller.cron; // 触发的表达式原文
const when = controller.scheduledTime; // 计划时间,毫秒时间戳
if (cron === "*/5 * * * *") {
ctx.waitUntil(cleanupExpired(env));
}
if (cron === "0 3 * * *") {
ctx.waitUntil(buildDailyReport(env, when));
}
}
};
controller.cron 是字符串,值和 wrangler.toml 里写的一模一样,用它做分支最直观。controller.scheduledTime 是这次计划触发的时间戳,比 Date.now() 更准——实际执行可能晚几十毫秒到几秒,用计划时间做幂等键更稳。
ctx.waitUntil(promise) 是这个入口里最容易漏的东西。处理函数返回后,运行时不会等你没 await 的 Promise 跑完,未挂住的异步任务会被取消。所以要么 await,要么 ctx.waitUntil()。区别是:await 会让这次执行等到完成(受 CPU 时间与时长限制),waitUntil 表示"我提交了,你负责跑完"。
4. 一个完整的例子
export default {
async scheduled(controller, env, ctx) {
const startedAt = Date.now();
ctx.waitUntil(run(controller, env, startedAt));
}
};
async function run(controller, env, startedAt) {
const res = await fetch("https://example.com/api/ping", {
headers: { "user-agent": "cf63-cron" }
});
const status = res.status;
// 记到 KV,供 /status 页面读取
await env.STATUS_KV.put(
"last-run",
JSON.stringify({
cron: controller.cron,
scheduledTime: controller.scheduledTime,
httpStatus: status,
finishedAt: new Date().toISOString(),
costMs: Date.now() - startedAt
})
);
await res.text();
}
把结果写进 KV 再提供一个读取接口,是最省事的"定时任务有没有跑成功"的观测方式。KV 的读写细节见 59-workers-kv-tutorial.md,更正规的观测手段见 66-workers-observability.md。
5. 本地怎么触发
定时任务不能等 5 分钟来测。wrangler dev 带一个测试入口:
wrangler dev --test-scheduled
启动后浏览器或 curl 打这个地址就能手动触发一次:
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"
URL 里的 cron 参数会变成 controller.cron 的值,所以分支逻辑能被完整测到。想模拟另一个表达式就换参数值,不用改配置。
注意这个入口只在 --test-scheduled 下存在,线上访问 /__scheduled 是普通的 404。它不会真的注册 cron,只负责调用一次 scheduled。
6. 常见问题
问:定时任务里能用 request.cf 吗? 答:不能。没有 HTTP 请求,就没有 cf 对象、没有 cf-connecting-ip、没有地理信息。需要地域数据时只能自己调外部接口,或者在 fetch 里采集后存起来。
问:任务跑超过时长限制会怎样? 答:会被中断。定时任务的执行时长与 CPU 时间都有上限,长任务要拆成多批,或者放到 Queues 里逐条处理。中间状态存起来,下一次 cron 接着跑。
问:错过一次触发会补跑吗? 答:不会补跑错过的计划点。如果任务需要"无论如何补一次"的语义,就得自己记录最后成功的时间戳,在下次触发时判断需要补多少,而不是假设 cron 一定准时且不丢。
小结
cron 触发器要三样东西对上:wrangler.toml 里 [triggers].crons 的表达式、代码里 scheduled 处理函数、以及 controller.cron 的分支判断。三处任意一处不一致,表现出来就是"任务没跑"或"跑错了分支"。
两个细节值得单独记:时间基准是 UTC,0 3 * * * 不是北京时间凌晨三点;异步任务用 ctx.waitUntil 挂住,否则处理函数一返回就被取消,日志里什么都看不到。本地调试用 --test-scheduled 加 /__scheduled?cron=,能省掉每次等 5 分钟的折磨。需要"每隔 N 秒"或"某个对象自己的定时器"时,Durable Objects 的 alarm 更合适,见 62-workers-durable-objects.md。
数据来源与检索时间
检索时间 2026-11-10。
| 来源 | URL | 对应数据 |
|---|---|---|
| Cron Triggers 配置 | https://developers.cloudflare.com/workers/configuration/cron-triggers/ | [triggers].crons 写法、UTC 基准、最小粒度(未抓取) |
| Scheduled Handler | https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/ | controller.cron / scheduledTime 与 ctx.waitUntil(未抓取) |
| 本地测试定时任务 | https://developers.cloudflare.com/workers/testing/local-development/ | --test-scheduled 与 /__scheduled 入口(未抓取) |
核验说明
- 未部署,未本地运行。本篇没有创建 cron 触发器,没有执行
wrangler dev --test-scheduled,配置与代码未经执行验证。 - 第 2 节的表达式语义、第 3 节的参数名、第 5 节的调试入口均依据官方文档的通用写法,三页文档都没有抓取,未逐条比对原文。
- 免费档的 cron 数量上限没有给出具体数字,因为未在控制台核对;需要精确值时以控制台与 56-free-tier-limits-cheatsheet.md 为准。
- 第 4 节的代码依赖 KV 绑定
STATUS_KV,实际使用时要在wrangler.toml里补[[kv_namespaces]],本篇省略了这段配置。 - 未做:没有测
scheduledTime与实际执行时间的偏差,没有测任务超时被中断的具体报错,没有测同一 Worker 多 cron 表达式并发触发的行为。