メインコンテンツへスキップ
Toolsbase Logo

Cron式完全ガイド: 構文・パターン集・言語別Parserライブラリ・落とし穴

Toolsbase編集部
CronCrontabスケジューラLinuxKubernetesGitHub ActionsQuartz

なぜCron式は何度書いても間違えるのか

定期実行ジョブを運用するチームには、必ずと言っていいほどcronにまつわる失敗談がある。0 0 30 2 * がいつまでも実行されないままバックアップが3か月止まっていた話、毎時実行のつもりが毎分実行になっていたKubernetes CronJobの話、6時実行と書いたつもりが時差で4時に発火していたGitHub Actionsの話——cronの構文は5フィールドと数個の特殊文字という極端に短い言語だが、その短さの裏に「ぴったり合っていないと意図通りに動かない」決定がいくつも隠れている。

このガイドでは、1970年代Bell Labs由来でほぼすべてのLinuxシステム・コンテナランタイム・CIプラットフォームが継承するcron構文を、5フィールドの意味、特殊文字とその落とし穴、UNIX cronとQuartz Schedulerの差、各言語のParserライブラリ、現場で繰り返し起こる落とし穴の順に解説する。本記事中の式はすべてCron式パーサーに貼り付ければ、次の10回の実行予定時刻を即座に確認できる。

短い歴史:Bell LabsからKubernetesまで

cron の原型は1975年、Bell LabsのBrian KernighanがVersion 7 Unix向けに書いたものである。1987年にPaul Vixieが書き直したVixie cronがBSD・Linuxディストリビューション・macOSの実装の基盤になった。POSIX仕様(IEEE Std 1003.1)でユーザーインターフェース(crontab コマンド)は標準化されたが、振る舞いの一部——特に日と曜日の関係——は実装依存のまま残された。

2000年代に入り、Java界隈ではQuartz Schedulerが登場した。5フィールド形式に「秒」を加えた6フィールド、L(最終)・W(最も近い平日)・#(第N曜日)など独自の特殊文字を導入した。SpringのSpring@Scheduled(cron = "...")アノテーションはこのQuartz方言の6フィールド形式を使う。プラットフォーム間でcron式をコピペしたときに最も多く事故が起きるのがこの違いである。6フィールドのSpring式をLinux crontabに貼り付けると、何のエラーも出さず無言で動かない。

現代のコンテナスケジューラ——Kubernetes CronJob、Vercel Cron、GitHub Actions——はいずれもUNIX 5フィールド形式を採用している。多くはタイムゾーンも既定でUTCに固定されており、これも頻出する驚きの原因になる。

5フィールドの構成

標準UNIX cron式は、5フィールドを空白で区切った形式である:

* * * * *
│ │ │ │ │
│ │ │ │ └─ 曜日(0-6、0=日曜。SUN-SATも可)
│ │ │ └─── 月(1-12、JAN-DECも可)
│ │ └───── 日(1-31)
│ └─────── 時(0-23)
└───────── 分(0-59)

各フィールドはワイルドカード *、単一値、範囲、リスト、ステップ値を受け付ける。Vixie cronをはじめ多くの実装は曜日フィールドで 7 を日曜の同義語として認め、内部的に 0 に正規化する。本サイトのパーサーもこの慣例に従う。

「分が先で曜日が最後」というフィールド順序は、新人運用担当者がもっとも頻繁に間違えるポイントである。覚え方としては「小さい単位から大きい単位の順で、曜日だけが横軸として末尾にぶら下がる」と整理しておくとよい。

特殊文字

意味を持つ文字は5種類しかない。これさえ覚えれば現実世界で出会うほとんどのcron式は読める。

アスタリスク(*

* は「このフィールドの全値」を意味する。* * * * * は「すべての分・時・日・月・曜日」、つまり毎分実行である。「常に」を意味する独立の記号はないため、フィールドへの無関心を表現する唯一の手段が * となる。

カンマ(,)— リスト

カンマは離散値の列挙である。0,15,30,45 * * * * は毎時 :00 :15 :30 :45 の4回実行。範囲との組み合わせも可能で、0,30 9-17 * * 1-5 は平日9時から17時まで毎時の00分と30分に実行される。

ハイフン(-)— 範囲

ハイフンは両端を含む範囲を表す。9-17 を時フィールドに置けば9時から17時まで(17時を含む)。標準のcronはラップアラウンドをサポートしない——22-2 と書いても「22時から2時まで」とは解釈されない。0-2,22-23 のように分割して書く必要がある。

スラッシュ(/)— ステップ

スラッシュはステップ値を導入する。*/5 を分フィールドに置けば「0分起点で5分ごと」、つまり 0, 5, 10, ..., 55。明示的な範囲との組み合わせもでき、0-30/100, 10, 20, 30。よくある誤読は「*/15 は時刻の途中から15分間隔」というもの。実際には毎時 :00 :15 :30 :45 の固定タイミングで発火する。

英字エイリアス

月と曜日のフィールドは3文字エイリアスを大文字小文字を区別せず受け付ける(JANDECSUNSAT)。0 9 * * MON-FRI0 9 * * 1-5 と同義。可読性は上がるが、すべてのParserが対応しているわけではないので、可搬性を重視するなら数値表記の方が安全である。

現場で頻出するパターン集

以下は、実際の運用でよく出会うパターン。式部分をCron式パーサーに貼れば、次の10回の実行時刻をローカルタイムゾーンで確認できる。

意味 典型的な用途
* * * * * 毎分 死活監視、開発用タスク
*/5 * * * * 5分ごと 頻繁なポーリング、ログローテーション確認
*/15 * * * * 15分ごと キャッシュ更新、軽量な同期処理
*/30 * * * * 30分ごと 中頻度のレポート
0 * * * * 毎時0分 時間別ダイジェスト、レート制限リセット
0 */2 * * * 2時間ごとの0分 定期バッチ
0 */6 * * * 6時間ごとの0分 四半日レポート
0 0 * * * 毎日深夜0時 日付ロールオーバー、ログアーカイブ
0 2 * * * 毎日深夜2時 データベースバックアップ(低トラフィック帯)
30 4 * * * 毎日4:30 オフピーク帯のメンテナンス
0 9 * * 1-5 平日9時 朝会リマインダー
0 9-17 * * 1-5 平日9〜17時の毎時 業務時間内の定期処理
0 0 * * 0 毎週日曜0時 週次バッチ
0 0 1 * * 毎月1日0時 月次の請求スナップショット
0 3 1 * * 毎月1日3時 月次レポート
0 0 1 */3 * 四半期ごと(3か月毎の1日0時) 四半期レビュー
0 0 1 1 * 元旦深夜0時 年次のロールオーバー、証明書更新

開発系のスケジュールでは */15 * * * * の頻度が最も収まりがよい——応答性と負荷のバランスが取れる。運用系では 0 2-4 * * * が「ユーザーが寝ている間に厄介な処理を片付ける」定番の時間帯である。

UNIX cron vs Quartz: 最大の混乱源

サポート対応で最も頻繁に見るバグが、QuartzをLinux crontabに貼り付ける(または逆)パターンである。

UNIX cronは5フィールド(秒なし)で、* , - / と英字エイリアスを受け付ける。Linux crontab、macOSのlaunchd相当機能、Kubernetes CronJob、Vercel Cron、GitHub Actionsはすべてこの方言である。

Quartzは6または7フィールドで、以下を追加する:

  • 秒フィールドを先頭に追加(0-59
  • L(最終)。日フィールドでは「月の最終日」、曜日フィールドでは 6L のように使い「月の最後の金曜日」を意味する
  • W(最も近い平日)。15W で「月の15日に最も近い平日」
  • #(第N曜日)。6#3 で「月の第3金曜日」
  • ?(無指定)。Quartzは日と曜日の同時指定を許さないため、片方を無指定にする際に使う

Spring Bootの @Scheduled(cron = "...") はQuartzの6フィールド形式を使うため、0 0 9 * * MON-FRI「平日9時」 であって「毎週月曜の9時毎分」ではない。先頭の 0 を残したままLinux crontabに移植すると、ジョブが永遠に動かなくなる。

簡単な見分け方:空白区切りで**ちょうど5フィールドならUNIX cron、6または7ならQuartzまたはrobfig/cron(Go)**だと考えればよい。

言語別Cron Parserライブラリ

自前でParserを書くチームは少なく、ほとんどがライブラリを使う。覚えておく価値のあるものを紹介する。

JavaScript / Node.js

  • node-cron: 軽量なインプロセスcronスケジューラ。5フィールド標準。3.x系でタイムゾーンサポートが追加された。
  • croner: 活発にメンテナンスされており、TypeScript優先。オプションで秒(6フィールド)対応、DST処理も明示的。
  • cron: 老舗ライブラリ。多くの既存コードベースで使われている。軽量だがDST挙動には注意。

Python

  • croniter: スケジューラではなくパーサー。式とタイムスタンプを与えると、次(または前)の実行時刻を返す。Airflow内部でも多用される。
  • APScheduler: cron・interval・dateトリガーを含む本格スケジューラ。秒含む6フィールド対応。

Go

  • robfig/cron(v3): Goデファクトのスケジューラ。**デフォルトは6フィールド(秒あり)**である点に注意。UNIX 5フィールドに切り替えるには cron.New(cron.WithParser(cron.NewParser(cron.Minute|cron.Hour|cron.Dom|cron.Month|cron.Dow|cron.Descriptor))) のように明示的にパーサーを指定する必要があり、これを忘れたためのバグはGo製サービスで本当によく見る。

Java

  • Quartz Scheduler: Java標準のスケジューラ。6または7フィールドで、Quartz方言(LW#?)を完全サポート。
  • Spring @Scheduled(cron = "..."): Quartzと同じ6フィールド形式だが、Spring独自のパーサーを使う(? はSpring 5.3+で対応)。

C# / .NET

  • NCrontab: .NET向けの古典的な5フィールドUNIX cronパーサー。軽量。
  • Cronos: Hangfireチームがメンテナンス。6フィールド対応、DSTとタイムゾーン変換を正しく扱う設計。本番でDSTを気にする場合の推奨選択肢。

Ruby

  • whenever: Ruby DSLからcrontabファイルを生成する。Rubyを書けば 0 9 * * MON を吐いてくれる。
  • rufus-scheduler: インプロセススケジューラ。cronと自然言語のスケジュール両方に対応。

プラットフォーム

  • Linux crontab: 5フィールド標準 + @reboot@hourly@daily@weekly@monthly@yearly のエイリアス。
  • Kubernetes CronJobspec.schedule): 5フィールド標準、既定でUTC。Kubernetes 1.27+ では spec.timeZone で明示指定可能。
  • GitHub Actionson.schedule.cron): 5フィールド標準で常にUTC、最短5分間隔、高負荷時は遅延することがある。
  • Vercel Cron: 5フィールド標準、デプロイ時に式を検証、UTC。

現場で繰り返し起こる5つの落とし穴

1. 存在しない日付

0 0 30 2 *——「2月30日深夜0時」——は永遠に実行されない。cronはエラーを返さず、該当しない日付の組み合わせを黙ってスキップする。何も壊れず、ただジョブが動かないだけなので、原因を追いかけるのが非常に厄介なバグになる。「このジョブ、本当に動くんだっけ?」と疑ったら、Cron式パーサーに貼り付けて次回実行プレビューが空であれば、その式は永遠に実行されないと一目でわかる。

2. 日と曜日のOR論理

標準Vixie cronでは、日と曜日を両方指定するとOR条件になる。0 9 1 * 1 は「毎月1日の9時 OR 毎週月曜の9時」に実行される。これを「毎月1日のうち月曜日のみ」(AND)のつもりで書いている人がほとんどである。回避策は、片方を * にしてアプリケーション側で制約を書く、もしくは ? で意図を明示できるQuartzを使うこと。

3. DSTとタイムゾーンの罠

UNIX cron式はシステムタイムゾーンで解釈される。サーバーが America/New_York0 2 * * * と書くと、時計が戻る日には2回実行(EDTで一度、ESTで一度)、進む日には一度も実行されない。Kubernetes 1.27+、croner、Cronosなどの現代のスケジューラはタイムゾーンを式と一緒に指定できる。バッチ処理ではUTC固定にし、「深夜」がレポート読者にとってどの時刻にあたるかは別途意識する方が安全である。

4. */N のリセット境界

*/15 を分フィールドに書くと 0, 15, 30, 45 で発火する——スケジューラ起動時刻からではなく、毎時0分起点で。14:07にスケジューラを起動した場合、最初の発火は8分後の14:15であって、15分後の14:22ではない。「今から15分ごと」というセマンティクスが必要な場合、cronはそもそも適さない(相対インターバル用ではないため)。

5. Quartz ? の「昨日まで動いていたのに」罠

0 0 9 ? * MON-FRI(日フィールドに ? を使い「日は曜日側に任せる」と明示するQuartz式)をLinux crontabに貼り付けると、? は不正な文字としてパース失敗——Parserによっては無言で失敗することもある。逆向きも同じで、Quartzのパーサーは * と特定曜日が同時に書かれると曖昧として ? を要求する場合がある。コピペする前にどの方言が動いているかを必ず確認すること。

モダンな代替手段

cronだけがジョブスケジューリングの選択肢ではない。直近10年でcronの役割の一部を侵食してきた代替を挙げる:

  • systemd timers: 現代のLinuxディストリビューションに組み込まれており、より豊かな構文(OnCalendar=Mon..Fri 09:00)、DST対応、journalctl 経由の優れたログ機能を持つ。新規のLinux専用サービスにはこちらが推奨。
  • launchd(macOS): 厳密にはcronではないが、macOSにおける同等機能。スケジュールはproperty list内の StartCalendarInterval キーで定義する。
  • Temporal / DBOS / Inngest: スケジューリングを機能の一部として持つ永続ワークフローエンジン。プロセス再起動を生き延びる・リトライ対応・内省が可能と、cronより厳密に強力だが、運用の複雑度は高い。
  • クラウドネイティブスケジューラ: AWS EventBridge Scheduler、Google Cloud Scheduler、Azure Logic Apps。マネージドインフラの上にcron風(多くはQuartz方言)の構文をラップしたもの。

多くのチームにとっては素のcron——コンテナ環境ならKubernetes CronJob——が依然として正しいデフォルトである。代替を検討する価値があるのは、リトライ・可観測性・分散協調が単純さより重要になる場面に限られる。

参考資料

本記事中の任意の式を実際に検証したい場合は、Cron式パーサーに貼り付ければ、次の10回の実行予定時刻をローカルタイムゾーンで計算し、人間が読める説明文と並べて表示する。