「バッチが動いていなかった」と後から気づく——そんな本番障害は珍しくありません。cronの死活監視を仕組みとして整える方法を、実案件の失敗談を交えながら解説します。
「バッチが動いていなかった」に気づくのは、なぜいつも遅いのか
こんな経験、ありませんか?
「月次の集計レポートがおかしい」とクライアントから連絡があり、調査してみると、2週間以上前からcronが止まっていた——。
Webアプリケーションの運用において、cronによる定期バッチ処理の停止は、発見が最も遅れやすい障害の一つです。Webサーバーが落ちていれば即座にアラートが飛びますし、データベース接続エラーならエンドユーザーが気づきます。しかしcronの停止は「何も起きていない状態」として静かに放置されてしまう。
LaravelのSchedulerを使った自動メール送信、WordPressのWP-Cronによるキャッシュクリア、カスタムスクリプトによる在庫同期——いずれも止まっていても画面上は正常に見えることが多く、ビジネス上の損害が積み重なってから初めて発覚するケースが後を絶ちません。
この記事では、cronの停止に気づくのが遅れる構造的な理由を整理したうえで、早期検知の仕組みを実装レベルで解説します。
あわせて読みたい
なぜ「cronが止まっている」発見が遅れるのか
停止してもエラーログが出ない
cronの問題は大きく2種類あります。
- cronデーモン自体が停止している(
crondやcron.serviceが落ちている) - cronは動いているが、実行スクリプトが内部でエラーになっている
前者は比較的わかりやすいですが、後者はやっかいです。cronはジョブを「起動したこと」は記録しますが、スクリプトが内部で例外を吐いて途中終了しても、デフォルト設定ではサーバー管理者のローカルメールに届くだけ。多くの本番環境でこのメールは誰にも読まれていません。
「成功の証拠」を監視していない
死活監視の世界には 「Deadman's Switch(デッドマンスイッチ)」 という概念があります。「定期的に生存報告がなければ異常とみなす」という考え方です。
多くのシステムでは「エラーが出たら通知する」というプッシュ型の監視しか設定されていません。しかしcronの停止は「何も起きない」という状態なので、プッシュ型では検知できないのです。
実案件での失敗談
以前、ECサイトのクライアントで在庫数をAPIから同期するLaravelバッチを本番運用していた際の話です。サーバー移行作業の際にcrontabの設定が引き継がれておらず、2週間にわたって在庫が更新されていませんでした。その間、売り切れ商品の注文が続いて、対応工数とクレーム対応が発生。最終的な損害は数十万円規模になりました。
監視の仕組みを後付けするのは、被害が出てからでは遅い。 この経験から、弊社では新規案件のすべてに以下の仕組みを標準で組み込むようにしています。
早期検知の実装:3つのアプローチ
アプローチ1:Healthcheck.ioやDeadMansSnitch(外部サービス)を使う
最も手軽で信頼性が高い方法です。Healthcheck.io(無料プランあり)などの外部サービスに「定期的にPingを送信し、来なければSlackに通知」という仕組みを作れます。
// app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
$schedule->command('app:sync-inventory')
->everyFifteenMinutes()
->thenPingOnSuccess(env('HEALTHCHECK_URL'));
// 成功時のみPingを送る = 失敗・未実行は検知できる
}
Healthcheck.io側では「15分以内にPingが届かなければアラート」と設定しておくだけで、cronが止まった瞬間から最大15分以内に通知が来ます。
アプローチ2:実行ログをDBに記録し、監視バッチで定期チェックする
外部サービスを使いたくない場合は、実行履歴をデータベースに記録する方法が有効です。
// app/Console/Commands/SyncInventory.php
public function handle(): int
{
$log = CronLog::create([
'job_name' => 'sync-inventory',
'started_at' => now(),
'status' => 'running',
]);
try {
// 実際の処理
$this->syncInventory();
$log->update([
'finished_at' => now(),
'status' => 'success',
]);
return Command::SUCCESS;
} catch (\Throwable $e) {
$log->update([
'finished_at' => now(),
'status' => 'failed',
'error' => $e->getMessage(),
]);
return Command::FAILURE;
}
}
続いて、監視用コマンドで「直近N分以内に成功ログがなければSlack通知」を実装します。
// app/Console/Commands/CheckCronHealth.php
public function handle(): void
{
$jobs = [
['name' => 'sync-inventory', 'interval_minutes' => 20],
['name' => 'send-daily-report', 'interval_minutes' => 1500], // 約25時間
];
foreach ($jobs as $job) {
$latest = CronLog::where('job_name', $job['name'])
->where('status', 'success')
->where('finished_at', '>=', now()->subMinutes($job['interval_minutes']))
->first();
if (! $latest) {
$this->notifySlack(
":warning: *{$job['name']}* が{$job['interval_minutes']}分以上実行されていません。"
);
}
}
}
この監視コマンド自体も5分おきにcronで動かし、さらにHealthcheck.ioで監視するという多段構成が理想です。
アプローチ3:Laravelのスケジューラー監視機能を活用する
Laravel 10以降では、schedule:monitorコマンドとLaravel Pulseを組み合わせることで、スケジューラーの実行状況をダッシュボードで視覚的に確認できます。
# スケジューラー監視を有効化
php artisan schedule:monitor
# Pulseのダッシュボードで確認
# config/pulse.phpに'schedule'カードを追加
すでにLaravelを使っているプロジェクトであれば、追加インフラなしで導入できる点が大きなメリットです。
よくある失敗パターンと対処法
サーバー管理、丸ごとお任せください
サーバー保守・運用
監視・障害対応・パフォーマンス改善まで、安定稼働をサポートします
※ 通常1営業日以内にご返信します
まとめと次のステップ
cronの停止検知は「エラーが出たら気づく」という受け身の監視では対応できません。「定期的に生存報告がなければ異常とみなす」というプル型の監視を、最初から仕組みとして組み込むことが重要です。
今すぐできることから始めるなら、まずはHealthcheck.io(無料)の登録とcrontabへのPing追加が最小コストで最大効果を得られます。既存のLaravelプロジェクトであれば、SchedulerのthenPingOnSuccess()を1行追加するだけです。
弊社Fivenine Designでは、新規案件はもちろん、既存システムの運用監視体制の見直しも承っています。「うちのサーバー、ちゃんと動いているか不安...」という方は、まずお気軽にご相談ください。