
Laravel 11のdeferヘルパーはキューの代わりになるのか——軽量非同期を再定義する境界線
ユーザー登録後のウェルカムメール、外部APIへの通知、アクセス解析データの送信、キャッシュの更新。どれも失敗したら即座に画面を壊す処理ではない一方、リクエストの途中で実行すると、ユーザーはその終了を待たされます。
これまでは、こうした処理を非同期化するならLaravelのキューを使うのが定番でした。ジョブを定義し、キュードライバーを選び、ワーカーを起動する。個人開発でも必要になれば避けて通れない構成ですが、たった一つの通知のためにRedisや常駐プロセスまで用意するのは、少し大げさに感じることもあります。
そこで登場したのが、[Laravel 11.23で追加されたdefer()です。defer](/articles/laravel-11nodeferguan-shuhacong/)()は、HTTPレスポンスをクライアントへ送ったあと、同じリクエストのPHPプロセス内で指定した処理を実行します。キューのように別のワーカーを用意する必要はなく、数行でレスポンス後の処理を登録できます。
一見すると、キューを簡略化したものに見えます。しかし、両者は「非同期」という同じ言葉でまとめるには性質が違います。defer()は処理を保存する仕組みではなく、レスポンス後に処理するための仕組みです。プロセスが途中で終了すれば、登録した処理も失われます。
defer()はレスポンスを速く返すための仕組みであり、ジョブを確実に処理するための仕組みではありません。
この境界線を曖昧にすると、画面は速くなったのに、メールや外部連携がいつの間にか消えているという、見つけにくい障害につながります。
Laravel 11.23で導入されたdefer()の基本動作と仕組み
defer()を理解するうえで大切なのは、登録したクロージャが別プロセスで動くわけではないという点です。
通常のLaravelのリクエストでは、コントローラーやミドルウェアの処理が完了し、レスポンスが組み立てられたあとに、クライアントへレスポンスが送られます。コントローラーの中でメール送信や外部API呼び出しを行えば、その処理が終わるまでレスポンスの送信も進みません。
defer()を使うと、レスポンス送信後に実行したい処理を登録できます。
たとえば、ユーザー登録処理のなかで次のように書きます。
defer(fn () => Mail::to($user)->send(new WelcomeMail($user)));
このクロージャは、defer()を呼び出した時点では実行されません。Laravelがリクエストの終了処理に登録し、レスポンスを返したあとに呼び出します。
ユーザー側から見ると、登録完了のレスポンスを先に受け取れます。その後、サーバー側ではウェルカムメールの送信処理が続きます。ここだけを見ると、確かに非同期処理らしく見えます。
ただし、キューとは違って、ここで作られたジョブがRedisやデータベースに保存されるわけではありません。defer()に登録された処理は、あくまで現在のリクエストに紐づいた処理です。
PHPのリクエストライフサイクルとの関係
Laravelのdefer()は、レスポンスを送信したあともPHPの処理を続けられる環境で効果を発揮します。PHP-FPMでは、環境によってfastcgi_finish_request()を利用してクライアントへのレスポンス送信と、サーバー内の後続処理を分けます。
この仕組みでは、クライアントへのレスポンスを先に完了させ、PHP-FPMのプロセス自体はその後も動作します。ブラウザはレスポンスを受け取って次の画面処理に進み、PHP側ではdefer()に登録されたクロージャが実行されます。
ここで注意したいのは、「レスポンスを送信した」と「処理が確実に完了した」は別だということです。
レスポンスを返したあと、次の処理に入る前にPHP-FPMのワーカーが停止したり、プロセスがメモリ不足で終了したり、サーバーのデプロイや再起動が発生したりすれば、クロージャは最後まで実行されません。defer()には、途中で失われた処理をあとから拾い直すための保存先がないからです。
また、レスポンス後に処理が走るとはいっても、PHPプロセスが別のワーカーへ引き継がれるわけではありません。リクエストを担当したプロセスが、そのまま後続処理も担当します。この点が、常駐ワーカーがジョブを取り出して実行するキューとの大きな違いです。
サーバー構成によって挙動は変わる
defer()の効果は、PHPの実行方式やWebサーバーの構成に左右されます。
PHP-FPMとFastCGIを組み合わせた一般的な構成では、レスポンス送信後に処理を続ける仕組みを利用できます。一方、Apacheのモジュール方式など、FastCGIを経由しない構成では、同じようにレスポンスを切り離せるとは限りません。
ローカル開発で使うphp artisan serve、開発用コンテナ、プロキシを挟んだ構成でも、本番と同じタイミングでレスポンスが送信されるとは限りません。ローカルでは処理が速く見えても、本番ではdefer()の処理がレスポンス送信前に実行され、待ち時間が短縮されない可能性があります。
これは、defer()が壊れているという話ではありません。レスポンスをどの段階でクライアントへ渡せるかが、Laravelだけで決まるわけではないということです。
本番導入前には、少なくとも次の点を確認しておくと安心です。
- 実際の本番と同じPHPの実行方式で動かしているか
- Webサーバーやリバースプロキシがレスポンスをバッファリングしていないか
defer()のコールバックがレスポンス送信後に実行されているか- コールバックが終わるまでPHP-FPMのワーカーを占有していないか
- クライアント切断時にも処理が継続する構成になっているか
curlなどでレスポンスの開始時間と、コネクションが完全に終了するまでの時間を比べるのも有効です。レスポンスの先頭が早く返っていても、サーバー側の処理が長く続けば、ワーカーはその間使われています。
「軽量」の意味を取り違えない
defer()が軽量だと言われるのは、Redisやデータベースなどのキューストアへジョブを書き込まず、別のワーカープロセスも必要としないからです。導入作業が少なく、個人開発の小さな構成に組み込みやすいのは確かです。
ただし、defer()のコールバックが軽量になるわけではありません。
コールバックの中で大量のモデルを読み込んだり、大きなファイルを処理したり、外部APIの応答を長時間待ったりすれば、その処理に必要なCPU時間とメモリは発生します。レスポンスを返したあとでも、その負荷が消えるわけではありません。
特にPHP-FPMでは、リクエストを担当したワーカーがdefer()の処理を終えるまで、次のリクエストにすぐ移れない場合があります。処理が短ければ問題になりにくいものの、重い処理を大量に登録すると、画面表示を速くした代わりにワーカーの空きが減ることがあります。
defer()を「サーバーの外で動く処理」と考えると判断を誤ります。実際には「同じリクエストの後半で動く処理」です。
キューとdefer()の決定的な違い:永続化とリトライ
defer()とキューの違いは、単に実行するタイミングだけではありません。
キューは、処理をあとで実行するために、ジョブの内容をキュードライバーへ渡します。データベースドライバーならデータベースに、RedisならRedisに、SQSならマネージドなキューサービスにジョブが保存されます。
その後、ワーカーがジョブを取り出して処理します。つまりキューは、処理を別の場所へ引き渡し、リクエストの終了後も残すための仕組みです。
永続化されるかどうか
defer()では、処理が現在のPHPプロセスの実行中に保持されます。プロセスが最後まで動けばコールバックも実行されますが、プロセスが途中で終了すれば、登録された処理は失われます。
キューでは、ジョブがキューストアへ正常に登録された時点で、Webリクエストから切り離されます。レスポンス送信後にWebサーバーが再起動しても、キューに保存されたジョブは残ります。
ただし、キューだから常に安全というわけではありません。ジョブがキューストアへ登録される前にリクエストが落ちれば、当然ジョブは残りません。キューへ登録されたあとも、ドライバーの仕様やワーカーの設定によって、処理中のジョブが再び取得可能になるまでの時間や、重複実行の可能性は変わります。
重要なのは、キューには「処理を保存して、あとから管理するための土台」があるということです。defer()にはその土台がありません。
OOMやワーカー停止時にfailed_jobsへ必ず残るわけではない
ここは誤解されやすい部分です。
キューで処理が失敗すると、設定された試行回数を超えたジョブがfailed_jobsテーブルへ記録され、あとからphp artisan queue:retry allで再実行できる、という説明を見かけます。これは、ワーカーが例外を捕捉してジョブの失敗をLaravelへ通知し、失敗処理が最後まで進んだ場合の話です。
ワーカーがメモリ不足で強制終了した場合や、プロセスが突然停止した場合、ジョブが必ずfailed_jobsへ記録されるわけではありません。失敗を記録する処理そのものが実行される前にプロセスが消えるからです。
実際の扱いは、キュードライバーと設定に依存します。
- ジョブが処理中として予約されている場合、ドライバーの可視性タイムアウトや
retry_afterの設定に応じて再取得されることがある - ワーカーのタイムアウト、ジョブの試行回数、再試行間隔の設定によって、再実行の回数やタイミングが変わる
- 例外をLaravelが捕捉して規定回数の試行を終えた場合は、
failed_jobsへ記録される - OOMやホスト停止など、失敗記録を残す前にプロセスやサーバーが終了した場合は、
failed_jobsに記録がないまま再試行や消失が起きる可能性がある - ドライバーによって、処理中ジョブの保持方法や再取得の条件は異なる
したがって、failed_jobsは「キューに入れた処理の失敗をすべて記録する履歴」ではありません。Laravelが失敗として処理できたジョブを記録する場所です。
php artisan queue:retry allも同じです。このコマンドが再実行できるのは、failed_jobsに保存されたジョブです。ワーカーが突然終了し、失敗記録が作られていないジョブを見つけてくれるコマンドではありません。
この違いを踏まえると、キューの設計ではfailed_jobsの監視だけでなく、ワーカーの終了、メモリ使用量、タイムアウト、再試行設定、ドライバー側の保持期間も確認する必要があります。キューはdefer()より堅牢ですが、設定なしで完全な配信保証になるわけではありません。
一方、defer()の場合はさらに手前で処理が失われます。コールバックがキューストアへ保存されていないため、プロセスが終了した時点で、再試行対象として確認する場所自体がありません。
リトライの有無
外部APIやメール送信は、処理そのものが正しくても一時的に失敗します。
SMTPサーバーへの接続が一度切れる、外部APIが一時的にエラーを返す、DNSの応答に失敗する。こうした障害は、少し時間を置いて再実行すれば成功することがあります。
キューでは、ジョブの試行回数や再試行間隔を設定できます。ジョブの種類に応じて、短い間隔で再試行するのか、長めの待ち時間を置くのかを調整できます。
defer()には、この再試行の仕組みがありません。クロージャの中で例外が発生しても、キューのように自動的に別の試行へ移るわけではありません。必要ならアプリケーション側で例外を捕捉してログへ残す処理を追加できますが、それはキューの再試行機能を持つこととは別です。
遅延実行とスケジュール
キューは、実行を未来の時刻へ移すこともできます。
たとえば、サブスクリプションの期限が近づいたユーザーへリマインダーを送りたい場合、期限の前にジョブをキューへ登録し、指定した時刻まで遅延させる設計が可能です。
defer()ができるのは、現在のレスポンスが返ったあとに処理することだけです。数分後、翌朝、指定日時といった時間軸は持っていません。
この違いをまとめると、次のようになります。
| 観点 | defer() | キュー |
|---|---|---|
| 主な目的 | レスポンス後に軽い処理を実行する | 処理を保存して別のワーカーで実行する |
| 永続化 | なし。現在のプロセスに依存 | あり。選択したドライバーに保存 |
| リトライ | 自動機能なし | 試行回数や再試行間隔を設定できる |
| 失敗記録 | 自動的な失敗ジョブ管理はない | 条件を満たして失敗処理が完了すればfailed_jobsへ記録 |
| OOM・強制終了時 | コールバックは失われる可能性が高い | 再取得されるか、失敗記録になるかはドライバーと設定に依存 |
| 遅延実行 | できない | delay()などで指定できる |
| 実行場所 | リクエストを処理したPHPプロセス | キューワーカー |
| 追加インフラ | 原則として不要 | ドライバーとワーカーの運用が必要 |
| 向く処理 | 消えても影響が小さい短時間の処理 | 失敗時に再実行したい重要な処理 |
defer()は、レスポンスを早く返す用途では便利です。しかし、信頼性を必要とする処理では、保存・再試行・失敗監視を持つキューのほうが適しています。
defer()を安全に使うための実行条件とalwaysメソッド
defer()は、どのレスポンスでも無条件に実行されるわけではありません。実行条件を理解せずに使うと、成功時には動くのに、バリデーションエラーや例外発生時には動かないという差が生まれます。
デフォルトでは成功したレスポンスが対象
通常のdefer()は、成功したレスポンスに対して実行される使い方を想定しています。
たとえば、ユーザー登録が成功したあとにアクセス解析イベントを送る、記事更新が完了したあとにキャッシュを更新するといった処理です。処理本体が失敗しているときに、成功を前提とした後続処理まで動かす必要はありません。
一方、失敗したリクエストでも必ず記録したい処理には、always()を使います。
defer(fn () => Analytics::track($event))->always();
always()を付けると、レスポンスのステータスに関係なく、レスポンス送信後にコールバックを実行する対象になります。
ただし、always()は実行の確実性を高める機能ではありません。あくまで「成功レスポンス以外でも実行対象にする」ための指定です。プロセスが停止した場合や、実行環境の都合で後続処理へ進めなかった場合まで救済するものではありません。
また、エラーレスポンスに対して実行する処理の内容にも注意が必要です。失敗した注文を売上集計へ送る、入力エラーを決済完了として外部サービスへ通知する、といった処理をalways()で無条件に動かせば、別の不整合を作ることになります。
クライアントが接続を切った場合
defer()はレスポンス送信後の処理なので、ユーザーがブラウザを閉じたあとも処理が続くように見えます。画面を離れたあとでもアクセスログを送る、といった用途には便利です。
ただし、ここでも「接続が切れても必ず完了する」と考えてはいけません。クライアントとの接続が切れることと、サーバーのPHPプロセスが最後まで生きることは別の問題です。
サーバーの再起動、コンテナの停止、PHP-FPMの再起動、メモリ不足による終了などが起きれば、クライアント切断後に実行していた処理も中断されます。
さらに、処理を続けている間はワーカーの資源を使います。PHP-FPMのpm.max_childrenが少ない環境で、複数のリクエストが長いdefer()処理を抱えると、新しいリクエストを処理できるワーカーが減ります。
レスポンスを早く返しているため、利用者からは快適に見えるかもしれません。しかし裏側では、PHP-FPMの待ち行列が伸びていることがあります。defer()の処理時間も、通常のレスポンス処理と同じように計測対象へ含めるべきです。
安全に使うための判断基準
defer()へ載せる処理は、次のような性質を持つものに絞ると扱いやすくなります。
- 実行時間が短く、処理中にワーカーを長く占有しない
- 失敗してもユーザーの購入、認証、契約などが不成立にならない
- 失われても、あとから集計や同期で補える
- 同じ処理が複数回実行されても結果が壊れにくい
- 失敗したときに、ログや監視で状況を確認できる
- 本番環境のPHP実行方式で、レスポンス後に処理されることを確認できている
ここでいう「短い」は、すべての環境に共通する固定値ではありません。小さなVPSで大量の同時アクセスを処理する場合と、余裕のある環境で低頻度に処理する場合では許容範囲が違います。
目安として、外部ネットワークの応答待ちが長くなる可能性のある処理や、数百ミリ秒を大きく超える処理は、キューへ移す候補です。時間だけで決めるのではなく、同時実行されたときにワーカーをどれだけ占有するかで考えます。
冪等性を意識する
defer()では自動リトライがありませんが、アプリケーションの変更や手動操作、外部サービス側の再送によって、同じ処理が複数回実行される可能性はあります。
たとえば、ユーザーの最終ログイン日時を現在時刻で更新する処理は、同じ入力で何度実行しても、結果が大きく壊れにくい処理です。一方で、ポイントを一定量加算する処理は、同じコールバックが二度走ると残高が二重に増える可能性があります。
決済、在庫、ポイント、予約枠のように、実行回数が結果へ直接影響する処理は、defer()を使う前に重複実行への対策が必要です。処理をキューへ移したとしても重複実行の問題が自動で消えるわけではありませんが、ジョブIDや冪等キー、トランザクションなどを組み合わせて管理しやすくなります。
ログを残す。ただしログだけを信頼しない
defer()のコールバックが走ったかを確認するには、処理の開始と終了、対象ID、外部サービスの応答結果などをログへ残します。
たとえば、解析イベントを送る処理なら、ユーザー情報をそのまま書き出すのではなく、イベントIDや内部の対象IDを記録しておくと、あとから欠落を調べやすくなります。
ただし、プロセスがログを書き込む前に終了すれば、ログも残りません。重要な処理について、ログがあることだけを根拠に実行保証と判断するのは危険です。
always()は失敗時にも実行対象にする指定であって、プロセス停止後の処理まで保証する機能ではない。個人開発におけるdefer()の活用シーンとパフォーマンス改善
defer()が活きるのは、処理の重要度が低いというより、失敗しても本体の処理を壊さず、あとから補える領域です。
アクセス解析や行動ログ
ページ表示、ボタン操作、フォーム送信などを外部の解析サービスへ送る処理は、defer()と相性がよい用途です。
解析イベントが一件欠けても、ユーザー登録や記事表示そのものが失敗するわけではありません。レスポンスを返すことを優先し、送信処理をあとへ回せます。
ただし、解析サービスへのHTTPリクエストが長時間待たされる場合は注意が必要です。タイムアウトを適切に設定し、送信先が応答しないときにPHP-FPMのワーカーをいつまでも占有しないようにします。
解析データが事業上重要で、欠落を許容できない場合は、最初からキューやデータベースへの一時保存を使うほうが適切です。
キャッシュの更新
記事を更新したあと、関連する一覧ページや人気記事のキャッシュを更新する処理も候補になります。
ユーザーへ記事更新のレスポンスを返したあと、次のアクセスに備えてキャッシュを温める設計です。キャッシュ更新に失敗しても、キャッシュが期限切れになれば次のアクセスで再生成できるなら、defer()の欠点を許容しやすくなります。
反対に、キャッシュが存在しないとアプリケーションが正しく動作しない場合や、更新の失敗で古い情報を長時間表示してしまう場合は、失敗時の扱いを別途考える必要があります。
自分向けの通知
デプロイ完了やバッチ終了をSlackやDiscordへ通知する処理は、個人開発では使いやすい例です。
通知が少し遅れたり、一度失敗したりしても、アプリケーションの本体には影響しないことが多いからです。通知を送るだけなら、処理時間も短く抑えやすいでしょう。
ただし、通知が監視の唯一の手段になっている場合は事情が変わります。障害通知が届かないと復旧が遅れるため、通知自体の信頼性が必要になります。その場合はキューや外部の監視サービスなど、別の経路を用意したほうがよいでしょう。
検索インデックスの更新
AlgoliaやMeilisearchなどへドキュメントの変更を伝える処理も、設計次第ではdefer()に載せられます。
検索インデックスが一時的に古くなっても、夜間処理や管理画面からの再同期で復旧できるなら、記事更新のレスポンスを優先できます。
ただし、「更新した直後から検索結果へ必ず反映される」ことが要件なら、単純なdefer()では不十分です。検索側の更新が失敗したことを検知し、再同期の対象に追加する仕組みが必要になります。
後続処理の分割
一つのリクエストの中で、ユーザーに直接関係する処理と、付随的な処理を分ける用途にも使えます。
たとえば、記事の保存そのものは同期的に完了させ、編集履歴の集計や管理者向けの通知だけをdefer()へ移します。こうすれば、レスポンスを遅くする原因を減らしながら、処理のまとまりを保てます。
ただし、データベースのトランザクションとの関係には注意してください。トランザクションが確定する前に、別の処理が保存されたことを前提に動いてはいけません。defer()へ登録する処理は、元の処理が正常にコミットされたあとに実行される前提を明確にしておく必要があります。
避けるべき処理
次のような処理は、基本的にdefer()ではなくキューなどの管理可能な仕組みへ移すべきです。
- 決済結果の反映や課金状態の更新
- 在庫の引き当て、予約の確定、ポイント残高の変更
- ワンタイムパスワードや認証用コードの送信
- 契約成立や注文完了を外部サービスへ伝える処理
- 法令、契約、社内業務上の記録として欠落が許されない処理
- 数秒以上かかる可能性があるファイル変換や外部API連携
- 失敗時に一定回数の再試行が必要な処理
判断の軸は、「処理が消えたら誰かが困るか」です。
困るとしても、あとから再構築できるなら、データベースへイベントを保存しておき、defer()では軽い通知だけを行う構成にできます。困ったときに復旧できる道筋がない処理を、リクエストの後半へ隠すのは避けたほうが安全です。
キューインフラが不要になるわけではない:設計上の境界線
defer()を使うと、RedisやSupervisorをすぐに導入しなくても、レスポンス後の処理を実装できます。これは個人開発にとって大きな利点です。
しかし、defer()を導入したからといって、キューインフラそのものが不要になるわけではありません。処理の重要度、実行時間、失敗時の復旧方法が変われば、必要な仕組みも変わります。
キューを選ぶ三つの問い
設計時には、次の順番で考えると判断しやすくなります。
1. 処理が失敗した場合、ユーザーや業務に取り返しのつかない影響があるか
決済、在庫、契約、認証などが該当します。影響が大きいなら、保存と失敗管理が必要です。
2. 処理が長時間実行される可能性があるか
外部APIの応答待ち、大きなファイルの処理、複数件のメール送信などは、レスポンス後であってもPHP-FPMのワーカーを占有します。実行時間が読みにくい処理は、キューへ分けたほうが運用しやすくなります。
3. 自動的に再試行したいか
一時的な通信障害を想定するなら、キューの試行回数や再試行間隔を使う価値があります。手動で再実行できる仕組みが必要な場合も、キューのほうが適しています。
この三つのどれかに当てはまるなら、defer()だけで済ませず、キューを使う方向で設計します。
個人開発での運用コスト
個人開発では、キューの導入コストも無視できません。
Redisを追加すればメモリを消費します。データベースドライバーを選べば、別サーバーを増やさずに済む一方、ジョブを保存するテーブルの管理や、ワーカーによる定期的な取得が必要です。SQSのようなサービスを使えば自前のキューストアを管理する負担は減りますが、外部サービスの設定と費用が発生します。
さらに、ワーカーの常駐、再起動、ログ監視、タイムアウト、メモリ上限、失敗ジョブの確認も必要です。failed_jobsに記録されたジョブを定期的に確認するだけでなく、ワーカーが落ちていないか、ジョブが滞留していないかも見なければなりません。
ここで大切なのは、運用コストを理由に、重要な処理までdefer()へ押し込まないことです。キューを使わない代わりに障害対応の手作業が増えれば、導入を省いた意味が薄れます。
段階的に導入する
個人開発なら、最初からすべてを複雑な構成にする必要はありません。
まずは、失敗しても影響が小さい処理をdefer()へ移します。アクセス解析、キャッシュ更新、自分向け通知などです。そのうえで、処理の欠落が問題になった領域や、実行時間が長くなった領域だけをキューへ切り出します。
キューへ移す処理は、ShouldQueueを使うジョブとして明示し、必要に応じて接続先やキュー名を分けます。高い優先度のメール送信と、急がない集計処理を同じ列に置かないだけでも、待ち時間の影響を抑えられます。
一方で、defer()を使う処理を無制限に増やすのも避けます。リクエストごとに外部APIを複数回呼び出したり、同じPHP-FPMワーカーに長い処理を積み上げたりすると、キューを導入していないことが別の形で負荷になります。
defer()からキューへ移行しやすい形にする
最初はdefer()で十分でも、サービスが成長するとキューが必要になることがあります。そのときに移行しやすいよう、処理本体をコントローラーのクロージャへ直接書き込みすぎないことが大切です。
外部APIへの送信や通知データの生成を専用クラスへ分けておけば、最初はdefer()から呼び、あとでジョブから呼ぶ形へ変更できます。処理の入力と副作用を整理しておくと、同期実行、defer()、キューのいずれにも組み替えやすくなります。
たとえば、コントローラーでは対象IDだけを渡し、処理クラスの中で必要なデータを取得する設計にします。ただし、defer()では元のリクエストの状態をそのまま参照できるため、リクエスト固有のオブジェクトや終了後に使えないリソースへ依存しすぎないようにします。キューへ移す場合は、シリアライズできる値だけで構成する必要があるからです。
defer()はキューの代替ではなく、手前の選択肢
Laravel 11.23のdefer()は、個人開発者にとって便利な機能です。レスポンスを遅くしていた小さな後続処理を切り離し、キューやワーカーを導入する前の段階でも、体感速度を改善できます。
ただし、仕組みはキューとは違います。
defer()はジョブを永続化しません。自動リトライもありません。失敗した処理をfailed_jobsへ記録して、あとから再実行する仕組みもありません。OOMやワーカー停止が起きた場合、処理が失われる可能性があります。
キューも、ワーカーの強制終了時に必ずfailed_jobsへ記録されるわけではありません。ドライバーの挙動、可視性タイムアウト、retry_after、試行回数、ワーカーのタイムアウトなど、複数の設定によって再取得や失敗記録の結果が変わります。それでもキューには、ジョブを保存し、処理状況を追跡し、条件が整えば再試行や失敗記録へつなげられる基盤があります。
この差は、処理の重要度が上がるほど大きくなります。
レスポンス後に実行できれば十分な処理はdefer()へ。失敗しても再実行できる必要がある処理、時間を置いて実行したい処理、処理の欠落を監視したい処理はキューへ。判断の基準は、非同期に見えるかどうかではなく、失敗したあとに何を要求するかです。
個人開発では、すべてを最初から重い構成にする必要はありません。だからこそ、defer()を便利な近道として使いながら、キューが必要になる境界線だけは曖昧にしないことです。
速さを優先してよい処理と、確実さを優先すべき処理を分ける。その判断ができていれば、defer()はキューを置き換える機能ではなく、Laravelの非同期処理に新しい浅い引き出しを増やしてくれる機能になります。
Related reading: Laravel 11のミドルウェア登録で判明した新設計と移行の注意点.