crontab の落とし穴 6 選:曜日と日付の OR、step の罠、タイムゾーン、サマータイム、秒なし、特殊文字列
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-month と day-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 式を書いたらレビューする項目:
- day-of-month と day-of-week の両方を指定していないか(OR 評価)
- step 表記の範囲が暗黙の
0-Nになっていないか - タイムゾーンが意図したものか(CRON_TZ や K8s timeZone を使う)
- DST 境界の時刻を避けているか
- 秒精度が必要なら cron 以外(systemd timer、Quartz)を選択
@rebootなどの特殊文字列がターゲット環境でサポートされているか
まとめ
cron 式の落とし穴は仕様の細かい挙動の認識ずれから発生します。とくに「day-of-month と day-of-week の OR 評価」は知らないと永遠に踏み続ける罠で、Linux の man crontab(5) でも明記されている仕様通りの挙動です。書く前にチェックリストを通す、書いた後は実機で 1-2 サイクル動作確認するのが、もっとも事故を減らす運用です。
cron 式の解析・次回実行時刻の確認は cron 式ツール で行えます。