GitHub Actions の cron が遅延する・動かない理由 — 日本時間の換算と実測データ

GitHub Actions のスケジュール実行を使うとき、最初にぶつかるのが「指定した時刻に動かない」問題です。毎朝9時に設定したはずのワークフローが、気づくと昼過ぎに走っている。私が1年近く動かしているデータ収集ワークフローの実測を見ると、1時間の遅れは日常、ひどい日は9時間近くずれました。
この記事では、UTCと日本時間の換算という基本から、実際にどれくらい遅れるのかの生データ、そして「遅れても壊れない」組み方までをまとめます。
まず結論
- cron の時刻指定は UTC。日本時間にするには9時間引く
- スケジュール実行はベストエフォート。混雑時は遅延し、まれにスキップされる
- だから「何時に動くか」に依存する設計をしてはいけない
- 「まったく動かない」場合は、遅延ではなく別の原因(後述の4つ)を疑う
UTC と日本時間の換算
cron に書く時刻は必ず UTC です。日本時間(JST)は UTC+9 なので、動かしたい日本時間から9時間を引いた値を書きます。

on:
schedule:
# 日本時間 9:00 = UTC 0:00
- cron: "0 0 * * *"
# 日本時間 18:00 = UTC 9:00
- cron: "0 9 * * *"
# 日本時間 6:00 = UTC 21:00(前日扱いになる点に注意)
- cron: "0 21 * * *"
引き算の結果がマイナスになるとき(例: 日本時間の朝6時 → 6 − 9 = −3)は、24を足して21時と書きます。このとき「日付が前日にずれる」ので、曜日指定を併用しているときは特に注意が必要です。日本時間の月曜6時は、UTCでは日曜の21時です。
実測:9時のつもりが、10時〜18時に動いていた
ここからが本題です。以下は私が運用しているワークフロー(cron: "0 0 * * *" = 日本時間9時のつもり)が、実際に発火した時刻の連続10日分です。
| 日付 | 実際の発火(JST) | 予定からの遅れ |
|---|---|---|
| 8/22 | 10:02 | +1時間02分 |
| 8/23 | 10:07 | +1時間07分 |
| 8/24 | 10:05 | +1時間05分 |
| 8/25 | 10:04 | +1時間04分 |
| 8/26 | 10:06 | +1時間06分 |
| 8/27 | 16:11 | +7時間11分 |
| 8/28 | 17:57 | +8時間57分 |
| 8/29 | 14:32 | +5時間32分 |
| 8/30 | 12:30 | +3時間30分 |

指定時刻ちょうどに動いた日は1日もありません。 平常時でも1時間前後、混み合う時期には半日近くずれます。無料枠だから遅いというわけではなく、スケジュール実行はそういう仕組みだと理解するのが正解です。
なぜ遅れるのか
GitHub のスケジュール実行は、キューに積まれて空いたランナーから順に処理されるベストエフォートの仕組みです。混雑時には遅延し、極端に混んでいる場合は実行がスキップされることもあります。
とくに毎時00分ちょうどは世界中のワークフローが集中する時間帯です。0 0 * * * や 0 * * * * のような「きっかり」の指定は最も混みます。少しでも遅延を減らしたいなら、7 0 * * * のように中途半端な分を指定するほうが有利です。
on:
schedule:
# 00分ちょうどを避ける
- cron: "17 0 * * *"
これで遅延がゼロになるわけではありませんが、混雑のピークを外す効果はあります。
「遅延」ではなく「まったく動かない」とき
数時間待っても実行された形跡がない場合は、遅延ではなく別の原因です。次の4つを順に確認してください。
1. デフォルトブランチに置いていない スケジュール実行は、デフォルトブランチ(main など)にあるワークフローファイルだけが対象です。作業ブランチに置いたままだと永久に動きません。意外とよくある原因です。
2. リポジトリの活動がなく自動停止された パブリックリポジトリでは、60日間コミットなどの活動がないとスケジュール実行が自動的に無効化されます。放置していたリポジトリで急に止まったなら、これを疑ってください。Actions の画面から手動で再有効化できます。
3. フォークしたリポジトリ フォーク直後のリポジトリでは、スケジュール実行は既定で有効になりません。Actions タブから明示的に有効化する必要があります。
4. YAML の書式ミス
on: の階層がずれている、cron の値をクォートしていない、といった書式のミスでもワークフロー自体が認識されません。cron 式は文字列としてクォートするのが安全です(- cron: "0 0 * * *")。
遅延を前提にした組み方
遅れるものだと分かってしまえば、対処は難しくありません。私は次の3つを守っています。
実行時刻に依存する処理を書かない
「9時に走る前提で、8時までのデータを集計する」のような時刻依存のロジックは壊れます。実行時点で取れるものを取る、あるいは日付だけを基準にする設計にします。何時に走っても同じ結果になれば、遅延は問題になりません。
二重実行されても壊れないようにする
遅延したジョブと次の回が近づくことがあります。同じデータを2回書き込んでも問題が起きないよう、保存先に一意制約を張っておくと安心です。
-- 同じ日に二重実行されても重複しない
unique (asin, category, captured_at)
この考え方は「GitHub Actions で毎日データを自動収集する」でも触れた、履歴を貯める設計と相性がいいものです。
手動実行の口を必ず開けておく
workflow_dispatch を書いておくと、Actions の画面からボタンひとつで即実行できます。「今すぐ動かして確認したい」ときにも、遅延しすぎて待てないときにも使えるので、スケジュール実行を書くときは常にセットで入れています。
on:
schedule:
- cron: "17 0 * * *"
workflow_dispatch:
まとめ
GitHub Actions の cron は、UTCで書く・時刻ちょうどには動かない・遅れても壊れないように作る。この3点さえ押さえれば、スケジュール実行は個人開発でとても頼りになる仕組みです。サーバーを持たずに毎日データが貯まっていく体験は、一度作ると手放せません。
実際にデータ収集ワークフローを組む手順は「GitHub Actions で毎日データを自動収集する」に、YAML の全文つきでまとめています。