crontab の落とし穴 6 選:曜日と日付の OR、step の罠、タイムゾーン、サマータイム、秒なし、特殊文字列

約8分

cron 式は 5 つの数字を並べるだけのシンプルさで、つい「ぱっと見で書ける」と過信しがちですが、実装で踏みやすい落とし穴が複数あります。「毎月 1 日の朝 5 時のつもりが毎日 5 時に動いていた」「step の */5 が時刻の起点を勘違いしていた」など、ほとんどが仕様の細かい挙動を読まずに書いた結果です。本記事ではよく踏む 6 つを整理します。

1. 曜日と日付の OR 関係(最大の罠)

cron の 5 フィールドは:

分 時 日 月 曜日

第 3 月曜日に実行」のような条件を以下のように書きたくなります:

0 5 15-21 * 1     # 「15-21 日」かつ「月曜(=1)」を意図

しかしこれは AND ではなく OR で評価されます。day-of-monthday-of-week の両方を指定すると、どちらかが一致すれば実行。実際には:

  • 15-21 日のいずれか(曜日問わず) → 実行
  • 月曜日(日付問わず) → 実行

つまり「毎月 15-21 日 + すべての月曜」で月に 11-12 回実行されます。

仕様の根拠

POSIX cron / Vixie cron は「day-of-month か day-of-week のどちらかで指定された場合、それぞれの条件で実行」と規定しています:

If both fields are restricted (i.e., aren’t *), the command will be run when either field matches the current time.

対策

  • 第 N 曜日のような条件はシェルスクリプト側で判定する:
0 5 15-21 * * test $(date +%u) -eq 1 && /path/to/job
  • crontab を書き直さない% がコマンドラインで \% にエスケープされるなどの細かい罠もあるため、シンプルな cron + シェル判定が読みやすい

2. step の起点を間違える

*/5 のような step 表記は「5 分ごと」と読みがちですが、正確には「範囲の最小値から step 飛ばし」です。

*/5 * * * *    # 0, 5, 10, 15, ... 55 分(OK、想定通り)

範囲を指定すると違います:

5/15 * * * *   # 5 分から 15 分ごと → 5, 20, 35, 50(35-65 ではない!)
0-30/10 * * * *  # 0, 10, 20, 30 分(30 を超えない)

毎時 5 分から 15 分間隔」と思って書いた 5/15 * * * * が、5, 20, 35, 50 で実行され、毎時の終わりごろにまとまることに。範囲を明示しないと暗黙の 0-59 になります。

対策

  • step は範囲なしで使う:*/15 * * * *
  • 範囲付き step は意図と一致するか必ず確認する。crontab は実際に走らせて最初の数回を見るのが安全

3. タイムゾーンの扱い

cron デーモンはサーバの localtime で動くのがデフォルト。Linux のサーバを UTC で運用している場合、「毎日 9 時 (JST) に実行」のつもりで 0 9 * * * と書くと、UTC 9 時 = JST 18 時に動きます。

対策

  • サーバが UTC のとき、JST 9 時を意図するなら 0 0 * * * と書く(時差 9 時間を引いた UTC 0 時 = JST 9 時)
  • 多くの cron 実装は CRON_TZ=Asia/Tokyo で個別 entry のタイムゾーン指定が可能:
CRON_TZ=Asia/Tokyo
0 9 * * * /path/to/job
  • Kubernetes の CronJob には spec.timeZone フィールドがある(K8s 1.27 以降 GA)
  • AWS EventBridge / Cloud Scheduler は明示的にタイムゾーンを指定するフィールドを持つ

タイムゾーンを意識しないとサマータイム(DST)境界で予測不能なズレが発生します。

4. サマータイム(DST)境界の二重実行・スキップ

DST のあるタイムゾーン(米欧の多く、日本にはなし)で、02:30 に実行する cron は:

  • 春の DST 開始:02:00 が 03:00 に飛ぶので、02:30 は実行されない
  • 秋の DST 終了:02:30 が 2 回来るので、2 回実行される

サブスクリプション課金の引き落とし、レポートの締め処理など、1 回だけ実行されることが重要な処理が、年に 1-2 回壊れます。

対策

  • DST 境界の時刻(02:00-04:00 あたり)は使わない。06:00 や 12:00 にする
  • どうしてもその時刻が必要なら、ジョブ側で冪等性を確保(同じ条件で 2 回走っても安全な実装)

5. 「秒」がない

POSIX cron の最小粒度はです。* * * * * * のように 6 フィールド書くと、多くの cron で構文エラー。秒指定がほしいときの代替:

  • systemd timer:秒単位の OnCalendar が書ける
  • Quartz cron(Java、Spring):6-7 フィールドで秒・年が書ける
  • AWS EventBridge / Google Cloud Scheduler:5 フィールド(秒なし)

「秒単位の cron」という機能名で実装が独自方言になっているため、ライブラリの仕様を必ず確認します。「Spring Boot の cron expression は Quartz 風の 6 フィールドだが、@Scheduled のフォーマットは 6 フィールドでも Quartz 厳密ではない」のような微差もあります。

6. 特殊文字列のサポート差

cron には人間が読みやすい特殊文字列があります:

@yearly      # 0 0 1 1 *
@monthly     # 0 0 1 * *
@weekly      # 0 0 * * 0
@daily       # 0 0 * * *
@hourly      # 0 * * * *
@reboot      # 起動時 1 回

@reboot の動作はデーモンによって異なります:

  • Vixie cron / cronie:起動時に 1 回実行
  • systemd-cron:実装依存
  • dcron:サポートなし

@reboot でログを出す cron を書いて、Docker 化したらコンテナ内に cron デーモンがいなくて動かなかった」も典型例。コンテナでは init system 系の restart: always を使う、systemd の OneShot=true ユニットにするなどの代替が必要。

チェックリスト

cron 式を書いたらレビューする項目:

  1. day-of-month と day-of-week の両方を指定していないか(OR 評価)
  2. step 表記の範囲が暗黙の 0-N になっていないか
  3. タイムゾーンが意図したものか(CRON_TZ や K8s timeZone を使う)
  4. DST 境界の時刻を避けているか
  5. 秒精度が必要なら cron 以外(systemd timer、Quartz)を選択
  6. @reboot などの特殊文字列がターゲット環境でサポートされているか

まとめ

cron 式の落とし穴は仕様の細かい挙動の認識ずれから発生します。とくに「day-of-month と day-of-week の OR 評価」は知らないと永遠に踏み続ける罠で、Linux の man crontab(5) でも明記されている仕様通りの挙動です。書く前にチェックリストを通す、書いた後は実機で 1-2 サイクル動作確認するのが、もっとも事故を減らす運用です。

cron 式の解析・次回実行時刻の確認は cron 式ツール で行えます。