KernelRanch
返回文章列表

Workers 定时任务:cron 触发器与 scheduled 处理函数

2026-11-10 #Cloudflare

读完这一篇,你能给一个 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 Handlerhttps://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 表达式并发触发的行为。