0 0 1 * 1 は「毎月1日の月曜」ではありません
まず答えから書きます。この cron 式は 「毎月1日」と「毎週月曜」の両方で実行されます。
0 0 1 * 1
│ │ └── 曜日:月曜
│ └──── 月:毎月
└────── 日:1日
「1日かつ月曜」を意図して書いたのに、実際は 月に5〜6回動きます。2026年8月なら 1日・3日・10日・17日・24日・31日 の6回です。
月次のバッチをこの書き方で仕込むと、請求処理が月5回走ることになります。私が実際に引き継いだシステムで起きていた事故です。
仕様としてそうなっています
POSIX の crontab 仕様に、こう書かれています。
日(day of month)と曜日(day of week)の両方が制限されている場合、コマンドはいずれか一方に一致したときに実行される
つまり判定はこうなります。
| 日 | 曜日 | 判定 |
|---|---|---|
* |
* |
毎日 |
1 |
* |
1日のみ(AND のように振る舞う) |
* |
1 |
月曜のみ(同上) |
1 |
1 |
1日 または 月曜(OR) |
片方が * のときだけ、期待どおりに動きます。 両方を指定した瞬間に OR に切り替わる、という非対称な仕様です。
これが厄介なのは、普段は正しく動いてしまう点です。0 3 * * 1(毎週月曜3時)も 0 3 1 * *(毎月1日3時)も期待どおり動くので、両方書けば絞り込めると考えてしまいます。
なぜこうなっているのか
歴史的な経緯です。初期の cron は「毎月15日」と「毎週金曜」のように、どちらかの周期で動かしたい用途を想定していました。両方を満たす日を指定する需要は想定されていません。
AND が必要になったのは、月末処理や「第2火曜」のような業務要件が出てきてからです。仕様が先にあり、要件が後から来た形です。
「毎月第1月曜」をどう書くか
cron 式だけでは書けません。スクリプト側で判定します。
#!/usr/bin/env bash
set -euo pipefail
# 毎週月曜に起動し、その日が第1週でなければ何もしない
day=$(date +%-d)
if [ "$day" -gt 7 ]; then
echo "第1月曜ではないため終了します(本日 $day 日)"
exit 0
fi
/usr/local/bin/monthly-report.sh
crontab 側はこうします。
0 3 * * 1 /usr/local/bin/first-monday.sh
曜日だけを指定し、日は * のままにするのが要点です。これで OR 判定を踏みません。
Python なら次のように書けます。
from datetime import date
def is_first_monday(today: date | None = None) -> bool:
today = today or date.today()
return today.weekday() == 0 and today.day <= 7
weekday() は月曜が 0 です。isoweekday() だと月曜が 1 になるので、取り違えないでください。
実装によって挙動が違う箇所があります
*/2 のようにステップ付きで全体を指定した場合、実装によって「制限あり」と判定されるものがあります。
Vixie cron(多くの Linux ディストリビューションが採用)は、フィールドが文字どおり * かどうかで判定しています。したがって */1 は「全ての日」を表しますが、* ではないため制限ありとして扱われ、OR 判定に入ります。
意図せず OR に入らないよう、制限しない意図のフィールドは * と素直に書くのが安全です。
動作を確認する方法
仕込む前に確認してください。設定を投入してから1ヶ月待って気づく、では遅すぎます。
1. 次の実行日時を計算する
Python の croniter が手軽です。
from croniter import croniter
from datetime import datetime
it = croniter("0 0 1 * 1", datetime.now())
for _ in range(5):
print(it.get_next(datetime))
2. 実際に流して確かめる
短い間隔の式に置き換え、ログが期待どおり出るかを見ます。本番に入れる前の数分で済みます。
* * * * * /usr/bin/logger -t crontest "fired"
journalctl -t crontest -f で追えます。確認が終わったら必ず消してください。
そのほか、cron で踏みやすい罠
環境変数が読み込まれません。 cron から起動されるシェルはログインシェルではないため、.bashrc も .profile も読まれません。手元で動くのに cron だと動かない原因の大半がこれです。スクリプト内で PATH を明示してください。
多重起動します。 前回の処理が終わる前に次が始まると、二重に走ります。flock で排他してください。
*/5 * * * * /usr/bin/flock -n /tmp/job.lock /usr/local/bin/job.sh
タイムゾーンはサーバー設定に従います。 UTC のサーバーで「毎日9時」と書くと、日本時間の18時に動きます。timedatectl で確認してください。
標準出力がメールで飛びます。 MTA が動いていないと、エラーに気づけません。ログファイルへリダイレクトするか、監視に送ってください。
確認用のツールを置いています
上記を毎回手で確かめるのが面倒なので、cron 式を貼ると日本語の説明と次の実行日時を返すツールを作って公開しています。
日と曜日を両方指定した式には、OR 判定になる旨の警告を出すようにしています。入力内容はサーバーへ送信せず、すべてブラウザ内で処理しています。
同じ場所に、実務で必要になって作ったものを置いています。
- Content-Security-Policy 生成ツール — 使っている機能を選ぶと CSP ヘッダーと nginx / Django の設定例が出ます
- nginx リダイレクト設定 生成ツール — 旧URLと新URLの表から
location/mapを生成します
まとめ
0 0 1 * 1は「1日または月曜」。AND ではありません- 片方を
*にしたときだけ、期待どおりに絞り込めます - 「第1月曜」のような条件は、曜日だけ指定してスクリプト側で判定します
- 制限しない意図のフィールドは、
*/1ではなく*と書いてください - 仕込む前に、次の実行日時を必ず確認してください