運営者がブログ運用のために作っているツール(Python製)には、決まった時刻に動く仕事がいくつかあります。朝にAdSenseの数字を取り込む仕事や、夕方に1日の報告をまとめる仕事です。このサイトの記事の作り方とツールの関係は運営者情報にまとめています。
この夕方の報告が、18:00の予定なのに18:54に作られた日がありました。原因はmacOSの launchd に設定していた起動間隔でした。
起きていたこと
2026年9月23日の記録です。18:08ごろに画面を見ると「日次報告の自動作成:正常」と出ているのに、今日の報告はまだありませんでした。報告は18:54に、定時実行として1枚だけ作られました。
ツールは常駐していません。launchd が一定の間隔で「判定」のコマンドを起動し、判定が「時刻を過ぎた仕事」を見つけて実行する作りです。報告を作る側の処理(時刻の判定、二重に作らない仕組み、タイムゾーン)には問題がありませんでした。
問題は判定の間隔です。8月23日に朝の取り込みのために設置したとき、間隔を3600秒(1時間)にしていました。朝の取り込みはその日のうちに動けばよい作りなので、1時間おきでも困りませんでした。実行ログでは、08:00の仕事が動いた時刻が日によって08:02から08:54までずれていましたが、気にしていませんでした。
9月23日は、17:54ごろの判定ではまだ18:00前なので何もせず、その次の判定が18:54でした。
間隔がそのまま最大の遅れになる
設置していた設定は、要点だけにすると次のような形です。ここではラベルとコマンドをダミーにしています。launchd には読み込まず、plutil -lint で書式だけ確かめました。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.blogtool.tick</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/true</string>
</array>
<key>StartInterval</key>
<integer>3600</integer>
</dict>
</plist>
$ plutil -lint dummy.plist dummy_calendar.plist
dummy.plist: OK
dummy_calendar.plist: OK
StartInterval は「N秒ごとに起動する」という指定で、何時何分に起動するかは決めません。最初に起動した時刻から1時間ごとなので、判定が毎時何分に来るかは設置したタイミングなどで決まります。
判定がどの位相で来ても、18:00の仕事は「18:00以降で最初の判定」で拾われます。これを1秒刻みで全部試す小さなシミュレーションを書きました。
# StartInterval で起動される「判定」が、18:00 に予定した仕事を何分遅れで拾うか。
# 判定の位相(最初に起動した時刻)は分からないので、0秒〜間隔の全パターンを1秒刻みで試す。
DUE = 18 * 3600
def first_tick_at_or_after(due, phase, interval):
# 判定は phase, phase+interval, phase+2*interval ... に起きる
k = -(-(due - phase) // interval)
return phase + k * interval
def fmt(sec):
return f"{sec // 3600:02d}:{sec % 3600 // 60:02d}"
print("記録の再現: 判定が毎時54分に来る場合(17:54 の次)")
t = first_tick_at_or_after(DUE, 17 * 3600 + 54 * 60, 3600)
print(f" 18:00 の仕事を拾う判定 = {fmt(t)} 遅れ {(t - DUE) // 60} 分")
print()
print(f"{'間隔(秒)':>8} {'最悪の遅れ':>10} {'平均の遅れ':>10}")
for interval in (3600, 600, 300):
lat = [first_tick_at_or_after(DUE, p, interval) - DUE for p in range(interval)]
m = max(lat); a = sum(lat) / len(lat)
print(f"{interval:>8} {m // 60:>5}分{m % 60:02d}秒 {a / 60:>8.1f}分")
記録の再現: 判定が毎時54分に来る場合(17:54 の次)
18:00 の仕事を拾う判定 = 18:54 遅れ 54 分
間隔(秒) 最悪の遅れ 平均の遅れ
3600 59分59秒 30.0分
600 9分59秒 5.0分
300 4分59秒 2.5分
1行目は記録と同じ状況(判定が毎時54分に来る)で、54分遅れになります。表のとおり、遅れの最大は間隔とほぼ同じ、平均はその半分です。3600秒なら最大で1時間近く、300秒なら5分以内です。
Macがスリープしていた場合
手元の man launchd.plist(macOS 26.6.2)には、StartInterval についてこう書かれています。
StartInterval <integer>
This optional key causes the job to be started every N seconds. If the
system is asleep during the time of the next scheduled interval firing,
that interval will be missed due to shortcomings in kqueue(3). If the
job is running during an interval firing, that interval firing will
likewise be missed.
スリープ中に来るはずだった起動は飛ばされます。時刻を指定する StartCalendarInterval のほうは、次のとおりです。
Unlike cron which skips job invocations when the computer is asleep,
launchd will start the job the next time the computer wakes up. If
multiple intervals transpire before the computer is woken, those events
will be coalesced into one event upon wake from sleep.
スリープ中に予定時刻を過ぎた場合は、起きたときに1回だけ起動します。AppleのScheduling Timed Jobsには、電源が切れていた場合は次の予定時刻まで実行されないことも書かれています。
18:00に1回だけ動けばよい仕事なら、StartCalendarInterval に Hour 18・Minute 0 を書くのが素直です(上の dummy_calendar.plist)。
ツールで選んだ直し方
このツールでは StartCalendarInterval にはせず、判定の間隔を300秒へ縮めて設置し直しました。
時刻の決まった仕事が複数あり、報告の時刻は画面から変えられます。時刻を過ぎていて、その日の分がまだなら作る、という取りこぼしの補い方もツール側に持っています。launchd を時刻ごとに設定すると、時刻を変えるたびに設定を書き換えることになるので、launchd は「短い間隔で起こす係」にとどめました。
あわせて次のようにしました。
- 設置コマンドは、600秒より長い間隔を断ります。今の設定を解除するより前に断るので、断ったせいで設置済みの設定まで消えることはありません
- 設置済みの設定ファイルから実際の間隔を読み、表示するようにしました。「設置済み」とだけ出ていても、何分遅れうるかは分からないためです
- 画面の「正常」は、機能がオンかどうかではなく、今日の報告が作成済み・作成待ち・遅延・失敗のどれかという実績で出すようにしました
- 予定から15分を過ぎて作った報告は、時刻どおりの分と区別して記録するようにしました
いちばん困ったのは、遅れていた間も画面が「正常」と出ていたことでした。壊れているのか、まだ時刻が来ていないだけなのか、画面からは区別できませんでした。
自分の設定で確かめること
StartIntervalの値を、その仕事が許せる遅れと比べたか(間隔=最大の遅れ)- あとから「時刻どおりに動いてほしい仕事」を足したとき、間隔を見直したか
- スリープ中の起動が飛ばされても困らないか。困るなら
StartCalendarIntervalか、ツール側の取りこぼし補完があるか - 動いた時刻をログに残しているか(判定の位相が日ごとにずれていないか)
- 「設定がオン」と「実際に動いた」を画面で分けているか
検証した環境
この記事の調査・再現・執筆は、運営者の指示のもとでAIエージェント(AnthropicのClaude)が行い、公開するかどうかは運営者が決めています。載せた出力は、そのAIエージェントがこのMacで実際にコマンドを実行して得たものです。
- macOS 26.6.2(man launchd.plist、plutil)
- Python 3.12.13(シミュレーションは標準機能のみ)
- 2026年10月19日に確認。launchd へは何も読み込んでいません