業務システム開発・保守の実務メモを発信中

Laravel failed_jobsを再実行してよいかpayloadとexceptionで判断する

Laravelの failed_jobs に失敗ジョブが残っていると、queue:retry で再実行できます。
ただし、失敗理由によっては、すぐ再実行しても同じところで止まります。

たとえば、請求金額と入金額が合わずに失敗したジョブは、再実行する前に対象データを確認した方が安全です。

この記事では、payloadexception から処理対象と失敗理由を確認し、再実行してよい失敗か、先にデータを確認すべき失敗かを分けていきます。

目次

Laravelのfailed jobとは何か

Laravelのキューでは、時間のかかる処理をJobとしてキューに積み、workerがあとで実行します。 そのJobが処理中に例外で止まると、Laravelは失敗したJobとして記録します。

この失敗したJobが、ここでいう failed job です。 database queueを使っている場合、失敗の記録は failed_jobs テーブルで確認できます。

キューで例外終了したジョブがfailed_jobsに残る

failed_jobs は、失敗した件数だけを見る場所ではありません。 どのJobが、どのqueueで、いつ失敗したのかをあとから確認するための記録です。

Laravel公式ドキュメントでも、失敗したJobの確認や再実行はキュー運用の手順として扱われています。

この記事で見るのは、主に idqueuefailed_atpayloadexception です。 id は再実行や削除の対象を指定するときに使います。 queuefailed_at は、どのキューでいつ落ちたのかを見るために使います。

payloadにはジョブ名と実行時の値が残る

payload には、Jobクラス名や、キューに積まれたときの値が入ります。 たとえば、請求番号、顧客コード、金額、処理フラグのような値です。

この値を見ないと、failed_jobs の1行がどの業務データを処理しようとしていたのか分かりません。 再実行する前にpayloadを見る理由は、戻す対象を取り違えないためです。

exceptionにはジョブが止まった理由が残る

exception には、Jobが止まったときの例外メッセージとスタックトレースが残ります。 全部を読む必要はありません。まず見るのは、先頭の例外名とメッセージです。

今回なら、Payment amount mismatch というメッセージを見ます。 これは一時的な接続失敗ではなく、請求金額と入金額が合わないという業務データ寄りの失敗です。

queue:retryはfailed_jobsのIDを指定して再実行する

php artisan queue:retry 1 は、failed_jobs のID 1 のJobを再実行するコマンドです。 便利ですが、IDがあるから戻してよい、という意味ではありません。

一時的なAPIエラーやタイムアウトなら、再実行で通ることがあります。 金額不一致やステータス不整合なら、同じ値で再実行してもまた失敗します。

ということで、この記事では、queue:retry の前に payloadexception を見て、どちらの失敗なのかを分けます。

今回動かすジョブとdispatch元を見る

ここから、失敗するジョブとdispatch元を確認します。 payload に残る値は、Jobに渡された値とつながっています。

今回使うのは、請求入金照合を想定した ReconcileInvoicePayment というジョブです。 artisanコマンドから請求番号と金額を渡し、workerで処理したときに金額不一致で失敗させます。

Jobクラスは請求金額と入金額を照合する

まずJob側です。 __construct で受け取った値が、キューに積まれたJobの中身になります。 handle がworkerから呼ばれ、金額が合わない場合に例外を投げます。

PHP
public function __construct(
    public readonly string $invoiceNo,
    public readonly string $customerCode,
    public readonly int $expectedAmount,
    public readonly int $paidAmount,
    public readonly bool $allowRetry,
) {
}

public function handle(): void
{
    Log::info('invoice reconciliation started', [
        'invoice_no' => $this->invoiceNo,
        'customer_code' => $this->customerCode,
        'expected_amount' => $this->expectedAmount,
        'paid_amount' => $this->paidAmount,
        'allow_retry' => $this->allowRetry,
    ]);

    if ($this->expectedAmount !== $this->paidAmount) {
        throw new RuntimeException(sprintf(
            'Payment amount mismatch: invoice=%s expected=%d paid=%d retry=%s',
            $this->invoiceNo,
            $this->expectedAmount,
            $this->paidAmount,
            $this->allowRetry ? 'yes' : 'no',
        ));
    }
}

このJobでは、expectedAmountpaidAmount が一致しないと失敗します。 失敗時の例外メッセージには、請求番号、想定金額、入金額、再実行フラグを入れています。

dispatch元コマンドが検証用の固定値を渡す

次にdispatch元です。 lab:dispatch-failing-invoice-job は、失敗するJobを1件だけキューへ積むartisanコマンドです。

PHP
#[Signature('lab:dispatch-failing-invoice-job')]
class DispatchFailingInvoiceJob extends Command
{
    public function handle()
    {
        ReconcileInvoicePayment::dispatch(
            invoiceNo: 'INV-2026-0806-001',
            customerCode: 'CUST-DEMO-042',
            expectedAmount: 120000,
            paidAmount: 119500,
            allowRetry: false,
        );

        $this->info('Dispatched ReconcileInvoicePayment with an intentional amount mismatch.');

        return self::SUCCESS;
    }
}

ここで渡している値が、あとで failed_jobs.payload に残ります。

この2つのソースからpayloadに残る値が決まる

ここまでで、実行導線はこうなります。

  1. lab:dispatch-failing-invoice-job を実行する
  2. DispatchFailingInvoiceJobReconcileInvoicePayment をdispatchする
  3. ReconcileInvoicePayment に請求番号と金額が渡る
  4. workerがJobの handle を実行する
  5. 金額不一致で例外が発生する
  6. Laravelが失敗Jobを failed_jobs に記録する

ここでは、ソース、dispatch、worker、failed_jobs の順に追います。 Jobに渡した値とfailed_jobsに残る値が同じ線でつながることを確認してから、再実行するかどうかを判断します。

queue workerでジョブを1回だけ失敗させる

ソースの流れを見たので、次は実際にJobを失敗させます。 ここでやることは3つだけです。

キュー用のテーブルを作り、検証用コマンドでJobをキューに積み、workerで1件だけ処理します。 大量のジョブを処理したいわけではなく、failed_jobs に1件残して中身を読むための実行です。

migrateでキュー用テーブルを用意する

まずmigrationを実行します。 今回の環境ではdatabase queueを使うため、jobsfailed_jobs のテーブルが必要です。

Bash
docker compose --env-file .env.example run --rm php php artisan migrate --force

ここではmigrationの詳細には踏み込みません。 この記事で見たいのは、失敗後に failed_jobs に何が残るかです。

dispatchコマンドでジョブをキューに積む

次に、さきほど見た DispatchFailingInvoiceJob を実行します。 このコマンドが ReconcileInvoicePayment をdispatchします。

Bash
docker compose --env-file .env.example run --rm php php artisan lab:dispatch-failing-invoice-job

この時点では、Jobはキューに積まれただけです。 まだ handle は実行されていません。

queue:work –onceで処理してFAILを確認する

workerを1回だけ動かします。 --once を付けると、キューから1件だけ取り出して処理します。

Bash
docker compose --env-file .env.example run --rm php php artisan queue:work --once --tries=1 --timeout=30

実行すると、今回のJobは金額不一致で失敗します。

Markdown
2026-08-06 00:08:50 App\Jobs\ReconcileInvoicePayment ............... RUNNING
2026-08-06 00:08:50 App\Jobs\ReconcileInvoicePayment ......... 218.44ms FAIL
tomo

FAIL を見ただけでretryすると、失敗理由を見ないまま同じ処理を戻すことになります。FAILだけを見てretryしない、という前提で進めます。

ここで FAIL になったので、次は failed_jobs を見ます。 workerの出力だけでは、どの請求が、どの値で失敗したのかまでは分かりません。

failed_jobsではまず対象行を絞る

failed_jobs を見るとき、いきなりpayload全文を読むとつらいです。 まずは対象行を絞ります。

見る順番は、idqueuefailed_at、Jobクラス、例外の先頭です。 ここで「どの失敗を読んでいるのか」を固めてから、payloadの中身へ進みます。

id、queue、failed_atで見る行を決める

まず、失敗Jobの基本情報を取ります。 長いpayloadとstack traceを全部出すと読みづらいので、SQLでは先頭だけを切り出します。

SQL
select
  id,
  uuid,
  connection,
  queue,
  left(payload, 900) as payload_head,
  left(exception, 1200) as exception_head,
  failed_at
from failed_jobs
order by id desc
limit 1;

記事本文では、まずこのあたりを見ます。

Markdown
id: 1
connection: database
queue: default
failed_at: 2026-08-06 00:08:50

id は、あとで queue:retry を実行するときに指定する値です。 ただし、この時点ではまだretryしません。

tomo

最初からpayload全文を読むより、まず一覧で対象行を決めた方が楽です。対象の idfailed_at が決まると、調査している失敗を見失いにくくなります。

displayNameとcommandNameでジョブクラスを見る

次に、payloadの先頭からJobクラスを見ます。 今回のpayloadには、次のような値が入っています。

Markdown
"displayName":"App\\Jobs\\ReconcileInvoicePayment"
"commandName":"App\\Jobs\\ReconcileInvoicePayment"

ここで、失敗したのが ReconcileInvoicePayment だと分かります。 さきほど見た請求入金照合Jobです。

この確認を挟むと、failed_jobs の1行が単なるエラーではなく、どの処理の失敗なのかに戻せます。

exceptionの先頭で失敗理由を確認する

exception の先頭には、今回こう残っています。

Markdown
RuntimeException: Payment amount mismatch: invoice=INV-2026-0806-001 expected=120000 paid=119500 retry=no

ここで見るのは、例外名とメッセージです。 今回の失敗は、接続タイムアウトではありません。 請求 INV-2026-0806-001 について、想定金額 120000 と入金額 119500 が合っていません。

同じ値のまま再実行しても、同じ金額不一致で止まる可能性が高いです。

payloadから再実行してよい値かを読む

次に payload を見ます。 exception だけでも金額不一致は分かりますが、payloadを見るとJobに渡された値を確認できます。

ここで大事なのは、payloadを全部きれいに読むことではありません。 再実行判断に必要な値を拾うことです。

invoiceNoで対象請求を特定する

payload内の data.command には、Jobに渡された値がシリアライズされて残っています。 今回の抜粋はこうです。

Markdown
O:32:"App\Jobs\ReconcileInvoicePayment":5:{
  s:9:"invoiceNo";s:17:"INV-2026-0806-001";
  s:12:"customerCode";s:13:"CUST-DEMO-042";
  s:14:"expectedAmount";i:120000;
  s:10:"paidAmount";i:119500;
  s:10:"allowRetry";b:0;
}

invoiceNo を見ると、対象請求は INV-2026-0806-001 です。 この値が分かれば、請求テーブル、入金テーブル、外部連携ログなどを追えます。

ちなみに実データを見ると、以下のような感じです。

expectedAmountとpaidAmountで金額差を見る

次に金額を見ます。

payload内の値読み取れる内容
s:14:”expectedAmount”;i:120000;請求側の想定金額は 120000
s:10:”paidAmount”;i:119500;入金側の金額は 119500

差額は 500 です。 この差が、今回の例外メッセージにも出ています。

ここまで一致しているなら、workerの一時的な失敗というより、Jobに渡した業務データの問題として扱う方が自然です。

allowRetryで今回のジョブ側の判断材料を見る

今回のJobには allowRetry という値があります。 Laravel標準の項目ではありません。 このJobでは、再実行対象として扱うかどうかを見るための判断材料にしています。

payloadでは、こう残っています。

Markdown
s:10:"allowRetry";b:0;

PHPのシリアライズ表現なので少し読みにくいですが、b:0false です。 この値からも、今回のJobはそのまま再実行する対象ではないと判断できます。

allowRetry はLaravel標準のpayload項目ではありません。 実際の現場では、自分のJobに入っている業務ID、ステータス、処理種別、再実行可否に関わる値を読み替えます。

今回の失敗はすぐretryしない

ここまで見ると、今回のJobはすぐ queue:retry する対象ではありません。 理由は、失敗理由が一時エラーではなく、金額不一致だからです。

queue:retry は便利です。ただし、実行前に止まった理由を確認しておく必要があります。

一時エラーならretry候補にする

retry候補にしやすいのは、一時的な失敗です。

たとえば、外部APIの一時的なタイムアウト、ネットワークの瞬断、短時間のDB接続失敗などです。 こういう失敗なら、同じJobを戻すことで通ることがあります。

ただし、その場合でも、二重送信や二重登録が起きない処理かは確認します。 queue:retry は「もう一度実行する」だけで、「安全に戻す」までは保証してくれません。

業務データ不整合なら先にデータを確認する

今回のような金額不一致は、先にデータを見ます。 請求側が正しいのか、入金側が正しいのか、途中の取込で欠けたのかを確認します。

ここを飛ばしてretryしても、同じ Payment amount mismatch で止まる可能性が高いです。 それどころか、Jobの中身によっては途中まで進んだ処理をもう一度動かすことになります。

今回の失敗は、queue:retry 1 の前に請求データと入金データを確認する失敗です。 業務データ不整合ならretryよりデータ確認が先です。

判断表でqueue:retryする前の確認を固定する

今回の値を表にすると、こうなります。

failed_jobsで見るもの今回の値判断
job_classApp\Jobs\ReconcileInvoicePayment請求入金照合ジョブなので、対象請求を確認してから再実行する
invoiceNoINV-2026-0806-001対象請求が特定できる
expectedAmount / paidAmount120000 / 119500金額不一致なので、このままretryしても同じ失敗になる可能性が高い
allowRetryfalse再実行前にデータ確認または補正が必要
exceptionPayment amount mismatch一時的な接続失敗ではなく、業務データの不整合として扱う

この表の見方は単純です。 payload で対象を特定し、exception で止まった理由を見ます。 その2つが業務データ不整合を指しているなら、先にデータを直すか、どちらの値が正しいかを決めます。

queue:retry は、そのあとです。

tomo

迷ったら、「再実行できそうか」より先に「止める理由がpayloadとexceptionに残っていないか」を見ます。ここを固定すると、retryの判断がかなりぶれにくくなります。

まとめ

Laravelの failed_jobs は、失敗したJobを再実行する入口になります。 ただし、IDだけ見て queue:retry すると、同じ失敗を繰り返したり、処理対象を確認しないまま戻したりします。

今回の例では、payloadexception から次のことが分かりました。

  • 失敗したJobは App\Jobs\ReconcileInvoicePayment
  • 対象請求は INV-2026-0806-001
  • 請求側の金額は 120000
  • 入金側の金額は 119500
  • 失敗理由は Payment amount mismatch
  • allowRetryfalse

この状態なら、すぐに php artisan queue:retry 1 はしません。 先に請求データと入金データを確認します。

failed jobを戻す前に見る順番は、これで十分です。

  1. failed_jobs の対象行を決める
  2. payload で処理対象を見る
  3. exception で失敗理由を見る
  4. 一時エラーか業務データ不整合かを分ける
  5. retryするか、先にデータを直すかを決める

queue:retry は最後の操作です。 先に payloadexception を読むだけで、戻してよい失敗と止めるべき失敗をかなり分けやすくなります。

よかったらシェアしてね!
  • URLをコピーしました!
目次