入门指南

定时任务没有报错,也没有跑:三条停摆规则与一套心跳

对照 GitHub Actions 的 schedule 与 Cloudflare Cron Triggers 的公开文档,把「平台没触发」「配置没生效」「时间不是你以为的那个」拆开,并给出一套能区分「来过」与「做对了」的心跳设计。

最贵的故障不是报错,而是当天的任务根本没有开始。

作者
AOPC 编辑部
发布
AOPC 编辑部
发布时间
更新
2026/10/26
核查
已复核
一个人在白板上把定时任务的触发、执行与心跳三层画成分层检查表
经营者责任与 AI 执行层的关系

先看答案

适合谁
  • 用 GitHub Actions 或 Cloudflare Workers 跑定时任务的人
  • 只有一个后台任务,却总在事后才发现它没跑的人
  • 已经有监控,但只看「有没有报错」的人
你会获得
先分清平台为什么不触发,再用心跳把「来过」和「做对了」分开。
发布时间
实质更新
发布
AOPC 编辑部

60 秒结论

一分钟看懂这篇文章

最贵的故障不是报错,而是当天的任务根本没有开始。

人负责

  • 决定哪些任务必须留痕
  • 设定可接受的延迟窗口

AI 负责

  • 比对计划时间与实际执行
  • 把失败写成可检索记录
  1. 列计划
  2. 设心跳
  3. 定窗口
  4. 留证据

直接答案

定时任务最贵的故障不是报错,而是**「什么都没发生」**:没有异常、没有告警、也没有产出,你直到有人问「今天的报表呢」,才发现它已经三天没跑。

这类故障通常不发生在你的代码里,而在三层之间:

  1. 平台没有触发——高峰延迟、队列满时丢弃、分支不对、长期无活动被自动停用;
  2. 触发了,但配置还没生效——定时规则的增删改需要传播时间;
  3. 触发了,但时间不是你以为的那个——默认 UTC、夏令时跳变顺延。

而多数「监控」只覆盖了另一件事:进程来过。它证明不了任务做对了,也证明不了产出存在。

下面先把两条公开文档里的规则逐条拆开,再给一套一个人也维护得动的心跳设计。

一、三种「没跑」,现象完全不同

你可能看到的现象 官方文档怎么写 该核对什么
某天少跑一次,任务日志里没有任何记录 GitHub:schedule 事件「can be delayed during periods of high loads」,高负载时段包括「the start of every hour」;负载足够高时「some queued jobs may be dropped」 平台运行历史里有没有这次运行;把计划时间错开整点
改了工作流,定时任务还是老样子,或者根本不跑 GitHub:只有工作流文件存在于默认分支时才会触发;「Scheduled workflows will only run on the default branch」,且跑默认分支的最新提交 文件是否已合并进默认分支;默认分支是不是你以为的那个
一个长期没人动的仓库,定时任务悄悄停了 GitHub:公开仓库「scheduled workflows are automatically disabled when no repository activity has occurred in 60 days」 工作流是否被停用;这条只写在公开仓库语境下
刚改了 Cron Trigger,第一次执行没按新时间 Cloudflare:新增/修改/删除触发器「may take several minutes (up to 15 minutes) to propagate to the Cloudflare global network」 部署时间与预期执行时间之间,是否留出了 15 分钟
本地 9 点跑,线上 17 点跑,或者某天时间跳了 GitHub 默认 UTC,可显式设 IANA 时区;DST 春季跳变时,被跳过的时刻「advance to the next valid time」(2:30 顺延到 3:00)。Cloudflare「Cron Triggers execute on UTC time」 计划时间写的是哪个时区;当天实际执行时间

这张表最重要的不是五条规则,而是它们的共同点:没有一条会以「任务失败」的形式出现在你自己的任务日志里。任务没启动,就没有日志可写;任务被延迟,日志只会晚一点出现。只有平台侧的运行历史,才知道「平台到底有没有触发」。

二、GitHub schedule 的四条硬规则,和一个 60 天开关

① 只在默认分支上跑。 官方写了两句:工作流文件必须存在于默认分支才会触发;「Scheduled workflows will only run on the default branch」,并且跑的是默认分支的最新提交。这有两个直接后果:在功能分支上改 cron 不会生效;默认分支上一个坏提交,会被下一次定时任务原样拿去跑。

② 最短 5 分钟一次。 官方原文是「The shortest interval you can run scheduled workflows is once every 5 minutes.」所以「每分钟检查一次」在这条路上不是配置问题,是平台不支持。

③ 默认 UTC,可以显式设时区。 不写 timezone 时按 UTC 解释。对 UTC+8 的团队,0 9 * * * 实际是北京时间 17:00——这类错位不会报错,只会让任务在错误的时间安静地成功。

④ 夏令时春季跳变时,被跳过的时刻顺延,不是跳过。 官方举的例子是 2:30 AM 顺延到 3:00 AM。按这条规则的直接后果:如果你的心跳窗口按「计划时间 + 10 分钟」设,这一天会误报一次「没跑」。这不是平台故障,是窗口设得太窄。

60 天开关只写在公开仓库的语境里:「In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days.」页面没有展开「repository activity」的判定口径,所以不要假设「只要有人 star 就算活动」——你无法从这页推出这个结论。能做的只有一件事:在平台侧定期确认工作流仍处于启用状态,而不是等它自己恢复。

最后一条是官方建议,不是保证:「To decrease the chance of delay, schedule your workflow to run at a different time of the hour.」错开整点降低的是延迟概率,不是把延迟变成零。

三、Cloudflare Cron Triggers:UTC、15 分钟传播、100 条历史

Cloudflare 这一侧的字更少,但有三处对「事后取证」很关键。

① 执行时间按 UTC。 页面只有一句「Cron Triggers execute on UTC time.」没有本地时区选项可写。所以同一个 0 9 * * *,在 GitHub 上可以配时区,在这里就是 UTC 9 点。

② 配置变更最多 15 分钟传播。 「Changes such as adding a new Cron Trigger, updating an old Cron Trigger, or deleting a Cron Trigger may take several minutes (up to 15 minutes) to propagate to the Cloudflare global network.」注意限定语:它说的是触发器的增删改,不是每次代码部署。把「每次部署都会延迟 15 分钟」写进 SOP 是误读。

③ 执行历史只留最近 100 次。 Cron Events 里「stores the 100 most recent invocations of the Cron scheduled event」;新建 Worker 或改 Worker 名字后,最多要 30 分钟事件才会显示。做个算术:每小时一次的任务,100 条大约覆盖 4 天;每天一次的任务大约覆盖 3 个月。需要更长的留存,页面指向 Workers Logs(保留更久、可筛选查询)或 GraphQL Analytics API。这意味着「上周三那次到底跑没跑」可能已经查不到了——所以关键任务的执行记录要落到你自己的存储里,而不是指望平台历史。

关于投递语义,这一页没有承诺。 全文没有「at least once」「guaranteed」这类表述,也没有写失败重试的次数或间隔。页面里唯一与重试相关的字样,是本地测试返回的 noRetry 字段——当 handler 调用 controller.noRetry() 时为 true。它说明「重试」这个概念存在,但没有给出线上重试的次数、间隔或时间窗口。所以任何「Cloudflare 会替你重试三次」的说法,都不该从这一页推出来。

四、心跳:把「来过」和「做对了」分开

一个人维护时,最容易犯的错是只写一条「开始执行」的日志,然后把它当监控。它回答的只是「进程来过」。

一套够用的心跳至少要有三件事:

  1. 开始时写一条(证明触发成功、记录实际开始时间);
  2. 成功结束时更新状态,并带上业务量(处理了多少条、生成了几个文件、写入了多少行);
  3. 失败时也写,状态为失败并留下错误摘要。

第 3 条最容易被跳过,但它决定了一件事:「失败」和「没跑」在你的数据里是否长得一样。如果只有成功才写记录,这两种情况都是「没有记录」,你就无法在告警时区分「该重试」还是「该查平台为什么没触发」。

然后是告警条件。它不是「收到失败就告警」,而是:

现在已超过「计划时间 + 宽限」,且没有一条成功记录 → 告警。

宽限该设多大?文档没有给 GitHub 的延迟上限,Cloudflare 的 15 分钟也只针对触发器配置变更。所以宽限只能按你自己的执行历史分布来定,不能假设「晚 5 分钟就是故障」。对每天一次的任务,把宽限设成小时级通常比分钟级更接近现实;对每小时一次的任务,宽限至少要小于一个周期,否则两次失败会合并成一次告警。

还有一条是编辑判断,但很实用:给业务量设一个正常区间。「跑了,但处理 0 条」和「没跑」是两种完全不同的故障,前者往往意味着上游空了、权限掉了、或者查询条件写错了。心跳里不带业务量,这两种情况在监控上都是绿的。

信号 能回答 不能回答
任务日志里的「start」 进程来过、什么时候来的 是否跑完、是否做对
成功心跳 + 业务量 跑完了,且处理了 N 条 处理的内容是否正确
平台运行历史 平台是否触发、这次运行的结论 你的下游是否真的收到产出
产出物校验(文件存在、行数、下游查询) 结果存在且形状对 结果在业务上是否正确

四行要一起看,缺一行就会留下一个「看起来是绿的」盲区。

五、上线前检查表

  • 计划时间写的是哪个时区? GitHub 默认 UTC(可设 IANA 时区);Cloudflare 只有 UTC。
  • 默认分支是哪一个? 工作流文件在不在默认分支?功能分支上的改动不会生效。
  • 是不是整点跑? 官方点名整点是高负载时段,能不能错开几分钟?
  • 间隔是否 ≥ 5 分钟? 这是 GitHub 的最短间隔。
  • 公开仓库会不会因 60 天无活动被停用? 谁负责定期确认它仍启用?
  • 改完 Cron Trigger 留了 15 分钟传播窗口吗? 别把首次执行时间卡在部署后立刻。
  • 有没有「开始」和「成功」两种记录? 只有一种,就无法区分没跑与没跑完。
  • 心跳带业务量了吗? 「0 条」和「没跑」区分得开吗?
  • 失败会写心跳吗? 不写,失败就和没跑一样安静。
  • 告警宽限 ≥ 你实测的最大延迟吗? 不要按「理论上应该很快」来设。
  • 上线后第一次执行,去哪个界面确认? Cloudflare 的 Cron Events 只有最近 100 条。

六、一周节奏(可照抄)

  • 上线当天:手动触发一次,确认 handler 真的被调用;再去平台侧运行历史里看到这一次记录。
  • 首周每天:只看两件事——成功心跳有没有出现、业务量是否落在正常区间。不要先看错误日志,因为「没跑」根本没有错误日志。
  • 每周:对一遍「计划次数」与「实际成功次数」。差值大于 0 的,逐条归因到:平台延迟/被丢弃/分支不对/配置未传播/时区错位。归因不了的那条,才是真正需要加监控的。
  • 每月:确认公开仓库的定时工作流仍处于启用状态;确认 Cron Events 里还能看到你需要的那段历史(看不到就说明该落到自己的存储了)。
  • 每季度:删掉没人看的心跳。一个人维护时,噪音不是来自告警太多,而是来自「设置了但从不处理」的告警。

七、三种误读

  1. 「cron 写了就会到点跑。」 官方明说会延迟,负载足够高时还会丢弃队列任务;此外还有默认分支、60 天停用、配置传播三个前置条件。它们共同的特点是:不报错。
  2. 「监控是绿的就说明任务成功。」 心跳只能证明进程来过;不带业务量的心跳,在「跑了但处理 0 条」时依然是绿的。成功与否要由「状态 + 业务量 + 产出物」三者共同回答。
  3. 「漏跑一次,补跑一下就好。」 补跑会改变下游看到的时间语义:按天分区的表、按小时聚合的报表、带「今天」条件的查询,都会因为补跑时间不同而写出不同的结果。补跑之前先确认下游是否幂等——这一条是编辑判断,不是平台文档的结论。

来源

本文所有引用均来自下列公开页面,访问日期均为 2026-10-26,发布前逐条重新验证为 HTTP 200:

  1. Events that trigger workflows – GitHub核查于 2026/10/26:schedule 事件在高负载时延迟、整点是高负载时段、队列满时可能丢弃、建议错开整点;工作流文件必须存在于默认分支、只在默认分支运行且跑最新提交;公开仓库 60 天无活动自动停用;默认 UTC、可选 IANA 时区;最短间隔 5 分钟;DST 春季跳变时顺延到下一个有效时刻(2:30 → 3:00)。
  2. Cron Triggers – Cloudflare Workers核查于 2026/10/26:按 UTC 执行;触发器增删改最多 15 分钟传播到全球网络;需要 scheduled() handler;Cron Events 只保留最近 100 次调用,新建或改名后最多 30 分钟才显示,更长的留存用 Workers Logs 或 GraphQL Analytics API;页面未披露投递保证与线上重试策略,仅本地测试返回 noRetry 字段。

唯一主行动

把下一步变成可执行动作

把定时任务纳入产品维护与回滚流程