先日、Laravelの管理画面を見ていたら、通知メールだけ妙に件数が合わない日がありました。
画面は正常終了し注文データも入っている。 でも、取引先から「メールが来ていない」と連絡が来て、少し嫌な汗が出ました。
サーバーで failed_jobs を見ると、通知系のJobが何件か失敗しています。 すぐ queue:retry したくなりますが、payloadを見ると対象の注文IDが入っていました。
この状態で雑に再実行すると、今度は二重通知になるかも、、ということで、慎重に対応する必要が出た、という話です。
この記事では、Laravelのfailed jobsを再実行する前に、payloadと例外ログをどこまで見た方がいいかを整理します。
failed jobsは、再実行コマンドを知っているだけでは少し危ないです。 payload で処理対象を見て、exception とアプリログで失敗理由を確認し、業務データが二重更新にならないかを見てから queue:retry します。
Laravelのfailed jobsとは何か
Laravelのキュー処理では、Jobが失敗すると failed_jobs に記録されます。 対象のJob、connection、queue、payload、exception、失敗時刻などをあとから確認できます。
ただし、failed jobsに残っているからといって、全部をそのまま再実行してよいとは限りません。 再実行してよいかどうかは、失敗理由と処理対象の状態を見てから判断します。
キュー処理が失敗したときに残る情報
まずはArtisanコマンドで失敗ジョブの一覧を見ます。
php artisan queue:failedこのコマンドでは、失敗したJobのID、connection、queue、失敗時刻などを確認できます。 Laravelのバージョンや失敗ジョブの保存先によって表示項目は変わるため、実際の環境で出ている列を基準にします。
DBの failed_jobs テーブルを直接見る場合は、次のような情報を確認します。
| 項目 | 確認する理由 |
|---|---|
| uuid / id | 再実行や削除の対象を指定するため |
| connection | database、redis、sqsなど接続先を確認するため |
| queue | 通知、取込、集計など処理の種類を分けるため |
| payload | Jobクラスや処理対象のIDを確認するため |
| exception | 例外クラス、発生箇所、エラーメッセージを見るため |
| failed_at | アプリログや外部サービスの障害時刻と突き合わせるため |
failed_jobs のカラムは、Laravelのバージョンや独自実装で違うことがあります。 記事のコマンド名だけを写す前に、まず手元のテーブル構造を確認します。
例外だけでなく最大試行回数やタイムアウトでも失敗になる
Jobは、単に例外を投げたときだけ失敗扱いになるわけではありません。 最大試行回数に達した場合や、タイムアウトした場合も失敗として扱われます。
たとえば次のような原因です。
- 外部APIが一時的に遅く、タイムアウトした
- DBロックやデッドロックで処理が失敗した
- 対象データが削除され、モデルを取得できなかった
- バリデーションや業務ルールで例外が出た
- リトライを繰り返して最大試行回数に達した
例外メッセージだけを見ると一時障害に見えても、実際にはデータ不整合が原因のことがあります。 逆に、コード不具合に見えても、外部サービスの障害が直れば再実行で通ることもあります。
すぐ再実行してよい失敗と、先に直す失敗がある
再実行してよい候補は、原因が一時的で、処理がまだ業務上完了していないと確認できるJobです。 たとえば、メールAPIの一時タイムアウトで、送信履歴にも外部側にも送信済み記録がない場合です。
先に直した方がいいのは、同じ条件で再実行してもまた失敗するJobです。 コード不具合、設定漏れ、存在しないID、権限不足、入力データ不正などは、再実行より原因修正が先です。
tomofailed_jobsが溜まっている画面を見ると、早く消したくなります。でも、消えたことと解決したことは別です。
再実行前にqueue:failedで対象を絞る
最初にやるのは、対象を絞ることです。 いきなり queue:retry all を打つと、原因も処理対象も違うJobをまとめて再実行してしまいます。
ID、connection、queue、失敗時刻を確認する
まず一覧を出します。
php artisan queue:failedこの結果から、再実行候補のID、connection、queue、失敗時刻を控えます。 失敗時刻は、Laravelログ、Webサーバーログ、外部APIの障害情報、DBの負荷状況と合わせて見ます。
DBで見るなら、まず新しい順に絞ります。
select
uuid,
connection,
queue,
failed_at
from failed_jobs
order by failed_at desc;環境によっては uuid ではなく id を使っていることがあります。 再実行コマンドに指定する値は、queue:failed に表示されるIDを優先します。
queue名で業務処理の種類を分ける
queue名は、何の処理が失敗したかを分ける手がかりになります。 emails、notifications、imports、reports、default のように分けている場合は、業務影響も違います。
たとえば、通知系の失敗とCSV取込系の失敗を同じ扱いにすると危ないです。 通知は二重送信、取込は二重登録、帳票生成は古いデータでの再生成が問題になります。
select
queue,
count(*) as failed_count,
min(failed_at) as first_failed_at,
max(failed_at) as last_failed_at
from failed_jobs
group by queue
order by failed_count desc;queue名ごとに件数と時刻を分けると、まとめて再実行してよい範囲が見えやすくなります。
まとめて再実行する前に件数を見る
失敗件数が数件なら、payloadとexceptionを1件ずつ見ても間に合います。 数百件ある場合は、同じ原因で失敗しているかを先に集計します。
select
queue,
left(exception, 200) as exception_head,
count(*) as failed_count
from failed_jobs
group by queue, left(exception, 200)
order by failed_count desc;同じ例外で大量に失敗しているなら、まず原因修正が先です。 外部API障害のように原因が解消済みなら、queueを絞って再実行する選択肢が出ます。
再実行対象は、ID、queue、失敗時刻、例外の傾向で絞ります。 all は便利ですが、業務処理では「全部まとめて戻す」が一番荒い操作になります。
failed_jobsのpayloadで処理対象を確認する
payload には、Jobの種類や処理対象の情報が入っています。 中身はJSONですが、Jobオブジェクトがシリアライズされているため、読みやすいとは限りません。
payloadからJobクラスと引数を探す
payloadを見ると、Jobクラス名やコマンド名に近い情報を確認できます。 たとえば、次のような形です。
{
"uuid": "9f5b5db0-1111-4444-9999-aaaaaaaaaaaa",
"displayName": "App\\Jobs\\SendOrderNotification",
"job": "Illuminate\\Queue\\CallQueuedHandler@call",
"maxTries": 3,
"timeout": 60,
"data": {
"commandName": "App\\Jobs\\SendOrderNotification",
"command": "O:30:\"App\\Jobs\\SendOrderNotification\":..."
}
}displayName や data.commandName でJobクラスを見ます。 maxTries、timeout、backoff などが入っている場合は、失敗理由と合わせて確認します。
LaravelのバージョンやJobの作り方によってpayloadの形は変わります。 Closureをキューに投げている場合や、独自ミドルウェアを使っている場合も、見え方が変わります。
モデルIDや注文番号などの業務キーを見る
payloadで一番見たいのは、どの業務データを処理していたかです。 注文ID、会員ID、請求ID、ファイル名、連携先IDなどが見つかれば、DB側の状態を確認できます。
ただし、シリアライズされた文字列から値を読むのは少し面倒です。 たとえばJobのプロパティに orderId を持っている場合、payload内の command に埋まっていることがあります。
O:30:"App\Jobs\SendOrderNotification":1:{s:8:"orderId";i:12345;}この例なら orderId が 12345 です。 このIDを使って、注文、通知履歴、外部連携ログを確認します。
select id, status, notified_at, updated_at
from orders
where id = 12345;
select order_id, channel, sent_at, provider_message_id
from notification_logs
where order_id = 12345
order by sent_at desc;業務キーが見つからないJobは、再実行判断が難しくなります。 今後の改修では、ログにJob名と業務キーを残すだけでも保守がかなり楽になります。
payloadを直接書き換える判断は慎重にする
検索すると、payload内のtimeout値やシリアライズされた値を直接書き換えて再実行する話が見つかります。 技術的にはできるケースもあります。
ただ、業務システムの保守では、payloadの直接編集は最後の手段に近いです。 シリアライズ形式を壊すと再実行できませんし、Jobの実装とpayloadの内容がずれると、別の失敗になります。
payloadを直接書き換える前に、同じJobを正しい引数で再投入できないか、専用の再実行コマンドを作れないかを先に検討したほうがいいでしょう。
payloadには個人情報や業務上の識別子が含まれることがあります。 調査メモやチャットへ貼る場合は、注文番号、メールアドレス、氏名、トークンなどを伏せてください。
exceptionとログで失敗理由を確認する
payloadで処理対象を見たら、次は失敗理由です。 exception カラムだけで分かることもありますが、同じ時刻の storage/logs/laravel.log も合わせて見ます。
exceptionカラムで例外クラスと発生箇所を見る
exception には、例外クラス、メッセージ、スタックトレースが入ります。 まずは先頭の数行を見ます。
select
uuid,
queue,
left(exception, 1000) as exception_head,
failed_at
from failed_jobs
where uuid = '9f5b5db0-1111-4444-9999-aaaaaaaaaaaa';見るところは、例外クラス、メッセージ、アプリ側のファイル名です。
GuzzleHttp\Exception\ConnectException: cURL error 28: Operation timed out
at app/Services/MailProviderClient.php:82
at app/Jobs/SendOrderNotification.php:47この例なら、メール送信先への接続タイムアウトです。 コード不具合より、外部APIの応答やネットワーク、タイムアウト設定を先に見ます。
laravel.logで同じ時刻のログを追う
failed_jobs.exception だけでは、前後の状態が分からないことがあります。 同じ時刻の laravel.log を見て、Job開始、対象ID、外部APIレスポンス、DB更新の有無を確認します。
grep "2026-08-05 09:12" storage/logs/laravel.log
grep "SendOrderNotification" storage/logs/laravel.log
grep "order_id=12345" storage/logs/laravel.logWindows環境なら、PowerShellで見ることもあります。
Select-String -Path .\storage\logs\laravel.log -Pattern "SendOrderNotification","order_id=12345"ログに業務キーが出ていない場合は、payloadやDBから補います。 exceptionは失敗理由、payloadは処理対象、アプリログは前後の流れを見るために使います。
外部API、DB、ファイル、メール送信のどこで止まったか分ける
失敗理由は、処理の種類ごとに見方を変えます。 通知、取込、集計、外部連携では、再実行のリスクが違います。
| 失敗箇所 | よく見る例外 | 再実行前に見るもの |
|---|---|---|
| 外部API | ConnectException、RequestException、timeout | 外部側の送信済み記録、連携ログ、リトライ制限 |
| DB | QueryException、Deadlock、Lock wait timeout | 対象レコード、更新済みフラグ、トランザクション境界 |
| ファイル | FileNotFound、Permission denied | ファイルの存在、権限、取込済み状態 |
| メール | SMTPエラー、APIタイムアウト | 送信ログ、重複送信防止キー、宛先 |
| モデル取得 | ModelNotFoundException | 対象データが削除済みか、再作成できるか |
この分類をせずに再実行すると、「一時的な失敗」と「再実行してもまた落ちる失敗」を混ぜて扱ってしまいます。
再実行してよい状態かを判断する
ここまで見てから、ようやく再実行するかを決めます。 判断は、Laravelの都合だけではなく、業務データの状態で決めます。
一時的な外部障害なら再実行候補にする
再実行候補になるのは、原因が解消済みで、処理がまだ完了していないと確認できるケースです。
たとえば次のような状態です。
- 外部APIがタイムアウトしていたが、現在は復旧している
- 送信ログに送信済み記録がない
- 対象データはまだ処理待ちステータス
- 同じJobが再実行されても一意キーで重複を防げる
この条件がそろうなら、1件だけ再実行してログを確認します。 いきなり全件戻すより、まず1件で通るかを見る方が安全です。
コード不具合やデータ不整合なら先に直す
同じ条件で再実行しても失敗するものは、先に原因を直します。 たとえば、存在しないカラムを参照している、設定値がない、必須データが欠けている、モデルが削除済みといった失敗です。
Illuminate\Database\QueryException: SQLSTATE[42S22]: Column not found
Illuminate\Database\Eloquent\ModelNotFoundException
ErrorException: Undefined array key "email"こういう例外は、再実行しても同じ場所で落ちます。 コード修正、データ補正、設定追加、Jobの作り直しを先に検討します。



再実行は便利ですが、原因修正の代わりにはなりません。同じ例外が並んでいるときは、コマンドより先に原因を潰します。
二重実行になる処理は業務データを確認する
一番怖いのは、Jobの途中まで処理が進んでいたケースです。 メールは送れているのにログ登録だけ失敗した、外部APIへ登録済みなのにDB更新だけ失敗した、という状態です。
この場合、再実行すると二重通知や二重登録になります。 通知、決済、在庫、請求、外部連携は特に注意します。
メール送信、決済、在庫更新、外部API登録のJobは、payloadだけで判断せず、業務テーブルと外部側の処理済み記録を確認してから再実行してください。
再実行してよいか迷うときは、次のように分けます。
| 状態 | 判断 |
|---|---|
| 原因が一時的で、処理未完了も確認できた | 1件ずつ再実行する |
| 原因は直したが、業務データの完了状態が不明 | 業務テーブルと外部ログを追加確認する |
| コード不具合や設定漏れが残っている | 修正してから再実行する |
| すでに処理済みで再実行すると二重処理になる | 再実行せず、必要なら失敗ジョブを削除する |
queue:retryで再実行し、結果を確認する
再実行してよいと判断できたら、queue:retry を使います。 コマンドを打ったあとも、成功したか、同じ例外で失敗していないかを確認します。
1件だけ再実行する
まずは1件だけ再実行します。
php artisan queue:retry 9f5b5db0-1111-4444-9999-aaaaaaaaaaaaLaravelのバージョンや保存方式によって、指定するIDが数値の場合とUUIDの場合があります。 queue:failed に表示されたIDを使うのが分かりやすいです。
再実行後は、キューワーカーが動いていることも確認します。 キューに戻しただけで、workerが止まっていれば処理は進みません。
php artisan queue:work --once
php artisan queue:failed本番環境で queue:work --once を直接実行するかどうかは、運用方式によります。 SupervisorやHorizonでworkerを管理しているなら、そちらの状態を確認します。
複数IDやqueue指定で再実行する
複数の失敗Jobを指定して再実行できます。
php artisan queue:retry 9f5b5db0-1111-4444-9999-aaaaaaaaaaaa aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee特定のqueueだけを再実行することもできます。
php artisan queue:retry --queue=notificationsqueueを指定する場合でも、対象queue内の失敗理由が同じとは限りません。 事前に件数と例外の傾向を見てから実行します。
retry allは最後の手段にする
Laravelでは、失敗ジョブをまとめて再実行できます。
php artisan queue:retry all便利ですが、業務システムではかなり強い操作です。 失敗理由が混ざっている状態で実行すると、同じ失敗を大量に繰り返したり、二重処理を起こしたりします。
retry all を使うなら、少なくとも次を確認します。
- 失敗理由が同じか
- 原因がすでに解消しているか
- 対象データが未処理状態か
- 二重実行を防ぐ仕組みがあるか
- 実行後に監視できる人がいるか
retry allは、調査を省くコマンドではなく、調査が済んだあとに使うコマンドです。



1件だけ再実行して通ったら、次に少数、最後に対象範囲を広げます。この順番にすると、戻す作業がだいぶ落ち着きます。
再実行しないfailed jobsの扱い
再実行しないJobもあります。 処理済みだったもの、再実行すると危ないもの、古すぎて業務上の意味がなくなったものです。
queue:forgetで1件だけ削除する
再実行しないと決めた1件は、queue:forget で削除できます。
php artisan queue:forget 9f5b5db0-1111-4444-9999-aaaaaaaaaaaa削除する前に、なぜ再実行しないのかを残します。 「送信済み確認済み」「手動対応済み」「対象データ削除済み」など、あとから見て分かるメモが必要です。
古い失敗ジョブはpruneやflushの前に残す基準を決める
古い失敗Jobを掃除するコマンドもあります。
php artisan queue:prune-failed
php artisan queue:prune-failed --hours=48
php artisan queue:flush
php artisan queue:flush --hours=48queue:prune-failed は古い失敗Jobを整理するためのコマンドです。 queue:flush は失敗Jobを削除する操作なので、実行前に対象を確認します。
残す基準がないまま掃除すると、障害の証跡が消えます。 運用では、保存期間、調査済みフラグ、外部連携の影響、再実行可否を決めておくと扱いやすくなります。
まとめ
Laravelのfailed jobsは、queue:retry で戻せます。 ただ、業務システムでは、戻せることよりも、戻してよい状態かを見分けることが大事です。
再実行前には、次を確認します。
queue:failedでID、connection、queue、失敗時刻を確認するpayloadでJobクラスと注文IDなどの業務キーを確認するexceptionとlaravel.logで失敗理由と発生箇所を見る- 通知、決済、在庫、外部連携では二重処理にならないかを見る
- まず1件だけ再実行し、結果を確認してから範囲を広げる
queue:retry all は、原因調査の代わりにはなりません。 failed jobsが溜まったときほど、payloadと例外ログを一度見てから動いた方が、あとで説明しやすい対応になります。



