Laravelのキューは、コントローラの1行をdispatchに置き換えるだけで動き始めます。ところが手元では動いたのに本番で消化されない、同じメールが2通届く、という詰まり方をしやすい機能でもある点に注意が必要です。原因の多くはコードではなく、ワーカーを常駐させる仕組みと再試行の設定値にあります。この記事は2026年8月30日時点の公式ドキュメントと配布ファイルの実測に基づく構成です。同期と非同期の切り分けから整理したいなら、非同期処理とは?同期処理との違いから実装方式まで実装者目線で解説を先に読むと前提が揃います。
まとめ:既定接続・常駐方式・再試行設定の判断
- 13系の既定キュー接続は
databaseです。Laravel 8から10期の記事にある「syncからdatabaseへ変える」という手順は、新規アプリでは前提が違います。 - ジョブは
make:jobで生成したクラスにhandleを書くだけです。難所はクラス側ではなく、ワーカーの常駐とタイムアウト設計に寄っています。 queue:workは起動しっぱなしが前提で、単体では落ちたら終わりです。本番ではsupervisorのようなプロセス監視と組み合わせます。- ワーカー側のタイムアウトは接続側の
retry_afterより必ず短くします。逆にすると同じ処理が2回走ります。 - 再試行回数やバックオフの指定は、13系では
Illuminate\Queue\Attributesの属性で書く形が中心です。従来のメソッド形式も残っています。 - ドライバ選定は「Redisをすでに運用しているか」と「キューを跨いだ可視化が要るか」の2軸で決まると判断しています。1台構成で流量が読めるうちは
databaseのままで足ります。
Laravel Queueはジョブ・キュー・ワーカーの3要素で動く
仕組み自体は素朴です。時間のかかる処理を1つのクラス(ジョブ)にまとめ、実行せずに待ち行列(キュー)へ積み、別プロセス(ワーカー)が順に取り出します。HTTPリクエスト内で完結させないので、画像変換や外部API呼び出しが遅くてもレスポンスを即座に返せるのが利点です。PHPという言語のレベルで非同期をどう実現するかはPHPの非同期処理とは|Fibers・ReactPHP・AMPHP・Swooleの違いと実装で扱っているので、ここではフレームワークが用意した仕組みに絞ります。
接続とキューは別物で、1つの接続に複数の名前付きキューが乗る
用語を整理します。config/queue.phpのconnectionsに並ぶのが「接続」で、ジョブの置き場(データベース、Redis、SQSなど)そのものです。その中に「キュー」という名前付きの区画があり、既定ではdefaultへ積まれます。highやemailsを作っておくと、ワーカー側で処理順を指定できます。
php artisan queue:work --queue=high,default
highを先に空にしてからdefaultへ移ります。優先度を分けたいだけならキュー名を分ければ済み、接続を増やす必要はありません。
13系の既定接続はdatabaseでjobsテーブルは初期状態にある
実際に配布されている13系のconfig/queue.phpを開くと、既定接続はデータベースになっています。
'default' => env('QUEUE_CONNECTION', 'database'),
'database' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false,
],
選べるドライバはsync、database、beanstalkd、sqs、redis、deferred、background、failover、nullの9種類です。末尾の3つは13系で加わった接続で、後半で触れます。
ジョブを貯めるjobsテーブルは、既定マイグレーション0001_01_01_000002_create_jobs_table.phpに含まれます。古い雛形を引き継いだプロジェクトなど、これが無い場合だけ生成コマンドを叩きます。
php artisan make:queue-table
php artisan migrate
RedisやSQSには追加パッケージが要ります。SQSはaws/aws-sdk-php ~3.0、Beanstalkdはpda/pheanstalk ~5.0、Redisはpredis/predis ~3.0かphpredis拡張です。ローカルにRedisコンテナごと用意するなら、Laravel Sailとは|sail up/downコマンドの意味と使い方・導入手順の構成をそのまま流用できます。
make:jobで定義したジョブをdispatchで積むまでの手順
実装は3手です。ジョブクラスを作り、handleに処理を書き、呼び出し側でdispatchします。
ジョブクラスを生成してhandleメソッドへ処理内容を集約する
生成はmake:jobです。app/Jobsが無ければ同時に作られます。
php artisan make:job ProcessPodcast
生成されたクラスはIlluminate\Contracts\Queue\ShouldQueueを実装しています。これが「同期実行せずキューへ積む」という合図です。インターフェイスを外したクラスはdispatchしてもその場で実行されます。
<?php
namespace App\Jobs;
use App\Models\Podcast;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
public function __construct(public Podcast $podcast)
{
}
public function handle(): void
{
// 変換処理など、時間のかかる本体をここへ置く
}
}
コンストラクタでEloquentモデルを受け取っている点に注意が要ります。モデルは積むときに主キーだけへ縮められ、ワーカー側で再取得される仕組みです。ペイロードは小さく保たれる一方、処理までの間にレコードが消えるとジョブは失敗します。シリアライズの前提はLaravel Eloquentとは|モデル定義・リレーション・eager loadingの基礎の取得の仕組みと地続きです。削除済みを無視してよいならDeleteWhenMissingModels属性で例外を出さず破棄できます。
dispatchの基本形と遅延・条件付きの書き分けを押さえる
呼び出し側は静的メソッド1本で、引数はそのままコンストラクタへ渡ります。
ProcessPodcast::dispatch($podcast);
// 条件付き
ProcessPodcast::dispatchIf($accountActive, $podcast);
ProcessPodcast::dispatchUnless($accountSuspended, $podcast);
すぐに処理させたくないときはdelayで遅延を付けます。外部APIのレート制限を避けたい場面や、猶予を持たせたい通知で使います。
短時間に同じジョブが何度も積まれる処理には、13系で入ったDebounceFor属性が効きます。検索インデックスの更新のように、最後の1回だけ実行できれば十分な処理を間引けます。
use Illuminate\Queue\Attributes\DebounceFor;
#[DebounceFor(30, maxWait: 120)]
class UpdateSearchIndex implements ShouldQueue
{
use Queueable;
public function debounceId(): string
{
return (string) $this->productId;
}
}
同じdebounceIdのジョブが30秒以内に再投入されると、先行分は破棄されて最後の1件だけが走ります。maxWaitは先延ばしの上限で、投入が続いても120秒経てば実行されます。重複を防ぎたいだけならShouldBeUniqueもありますが、ロック対応のキャッシュドライバが前提です。
トランザクション内でdispatchすると取りこぼしが起きる
代表的な落とし穴は、トランザクション内でのディスパッチです。コミット前にワーカーが拾ってしまう場合があります。ワーカーは別プロセスなので、未コミットのレコードは見えず、「対象が見つからない」という失敗だけが残ります。
'database' => [
'driver' => 'database',
// 中略
'after_commit' => true,
],
接続設定のafter_commitをtrueにすると、開いているトランザクションがコミットされるまでディスパッチが保留されます。ジョブ単位ならafterCommitメソッドを鎖状に付けます。既定はfalseなので、トランザクションを多用する業務システムでは接続側でtrueに倒すほうが事故が減るでしょう。
queue:workをsupervisorで常駐させる本番構成の作り方
ジョブを積んでも、取り出す側が動いていなければ何も起きません。本番でつまずく最大の箇所です。
queue:workの主要オプションと既定値を本番運用前に押さえる
起動はqueue:workで、手動で止めるまで走り続けます。
php artisan queue:work redis --queue=emails --tries=3 --backoff=3 --timeout=60
実測した既定値と主要オプションを整理します。試行回数の既定が1回である点は、再試行が効かないという問い合わせの典型的な原因です。
| オプション | 既定 | 効果 |
|---|---|---|
| tries | 1回 | ジョブの最大試行回数 |
| timeout | 60秒 | 超過でワーカーを異常終了 |
| backoff | 即時 | 失敗後の再投入までの秒数 |
| sleep | 3秒が慣例 | ジョブが無いときの待機 |
| max-jobs | 無制限 | N件処理したら終了する |
| max-time | 無制限 | N秒経過したら終了する |
| stop-when-empty | 無効 | 空になったら正常終了 |
| once | 無効 | 1件だけ処理して終了 |
件数と時間の上限は、長時間走ったワーカーが抱えるメモリを解放する仕掛けです。プロセス監視と組み合わせ、一定間隔でワーカー自身に終了させて再起動を任せます。
開発中はqueue:listenも使えます。再起動なしでコード変更が反映される代わり、毎回フレームワークを起動し直すため本番には向きません。
supervisorの設定ファイルでワーカーを落ちないようにする
queue:workはタイムアウト超過やqueue:restartで終了します。放置されないようプロセス監視を挟むのが定番で、Linux環境ではsupervisorを使います。設定ファイルはconf.d配下です。supervisorが受け取る標準出力と、アプリがフレームワーク経由で書くログは別系統です。後者のチャンネル設計とファイル権限はLaravelのログ設定|logging.phpのチャンネル設計と保持期間・マスキングで扱っています。
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/forge/app.com/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=8
redirect_stderr=true
stdout_logfile=/home/forge/app.com/worker.log
stopwaitsecs=3600
numprocsが同時に走らせるワーカー数で、この例では8プロセスが並列に処理します。並列数を上げるほど消化は速くなりますが、外部APIのレート制限やデータベース接続数の上限に先にぶつかるため、下流の限界から逆算して決めます。
見落とされやすいのがstopwaitsecsです。最長ジョブの実行時間より短いと、停止の途中でsupervisorがプロセスを強制終了し、処理中のジョブが中断されます。公式例が3600秒と大きめなのはそのためです。
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start "laravel-worker:*"
サーバのプロビジョニングごと任せるなら、supervisorの設定まで見るマネージド寄りの選択肢もあります。範囲と費用はLaravel Forgeとは|自動化される範囲・料金・Cloudとの選択基準にまとめています。
デプロイのたびにqueue:restartでワーカーを入れ替える
ワーカーは起動時のアプリケーションをメモリへ読み込んだまま走り続けます。デプロイでコードを差し替えても、走っているワーカーは古いままです。直したはずのバグがキュー経由でだけ再現する現象の多くはこれが原因です。
php artisan queue:restart
処理中のジョブを終えたところで各ワーカーを行儀よく終了させるコマンドです。立ち上げ直すのはsupervisorの役目なので、プロセス監視とセットで初めて機能します。再起動の合図はキャッシュ経由で伝わるため、キャッシュドライバの設定が前提です。デプロイ手順の順序はLaravelのキャッシュクリア|cache:clearとoptimize:clearの違いと本番デプロイの手順に沿わせます。
常駐プロセスが状態を持ち越す性質は、HTTPリクエスト側でも同じです。そちらの設計はLaravel Octaneとは|ワーカー常駐の仕組みと状態リーク・採用条件で扱っています。
Horizonを入れるのはRedisで運用を可視化したいときだけ
ワーカー設定をコードで管理し、スループットや失敗を画面で追いたくなったらHorizonが候補です。laravel/horizonの最新は執筆時点で5.48.3(2026年8月10日公開)で、13系に対応します。
'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default', 'notifications'],
'balance' => 'auto',
'autoScalingStrategy' => 'time',
'minProcesses' => 1,
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
],
],
],
balanceをautoにすると、キューごとのプロセス数を自動で振り分けます。基準はautoScalingStrategyで決まり、timeは空にするまでの推定時間、sizeは件数を見ます。balanceMaxShiftとbalanceCooldownは増減の速さで、この例では3秒ごとに最大1プロセスです。
採用の前提は明確です。Horizonはキュー接続がRedisであることを要求し、Redis Clusterには対応していません。Redisが無い案件では選択肢に入らず、逆に失敗ジョブの追跡へ人手をかけているなら入れる価値があります。Redis互換の実装での差分はValkeyとは?Redisとの違い・導入方法・データ型を解説【2026年版】で整理しました。メトリクスとして監視基盤へ流すなら、LaravelでPrometheus+Grafana監視を実装|メトリクス収集・可視化・アラート設定の構成のほうが既存の監視と揃います。
失敗ジョブをfailed_jobsで捕捉して再試行する組み方
ジョブは失敗します。外部APIの障害、タイムアウト、想定外のデータと理由は尽きません。設計すべきは「失敗しないこと」ではなく「失敗が記録され、拾い直せること」です。
試行回数とバックオフは属性で書くのがLaravel 13系の書き方
試行上限を超えたジョブはfailed_jobsテーブルへ入ります。上限はコマンドラインとジョブクラスの両方で指定できますが、13系の公式が前面に出すのは属性による指定です。
use Illuminate\Queue\Attributes\Backoff;
use Illuminate\Queue\Attributes\MaxExceptions;
use Illuminate\Queue\Attributes\Timeout;
use Illuminate\Queue\Attributes\Tries;
#[Tries(25)]
#[MaxExceptions(3)]
#[Timeout(120)]
#[Backoff([1, 5, 10])]
class ProcessPodcast implements ShouldQueue
{
// ...
}
Triesが試行回数、MaxExceptionsが例外を許容する回数です。分けて書けるのは、レート制限で解放されただけの再試行と例外で落ちた再試行を区別したいためです。Backoffに配列を渡すと1回目1秒、2回目5秒、3回目以降10秒と段階的に待ちます。外部APIの一時障害にはこれが効きます。
従来のtries()やbackoff()やretryUntil()というメソッド形式も残っています。13系へ上げても書き換えは不要ですが、新規なら属性のほうが宣言が1か所にまとまって読みやすいと考えています。
retry_afterとタイムアウトを逆にすると2回実行される
ここが最も事故につながる設定です。retry_afterは接続側の設定で、処理中のまま指定秒数が過ぎたジョブをキューへ戻します。ワーカー側のタイムアウトは、指定秒数を超えたジョブを抱えるワーカー自体を異常終了させる動きです。
前後関係が逆になると何が起きるか。retry_afterが90秒、タイムアウトが120秒なら、90秒でジョブが別ワーカーへ再投入され、そのあとも元のワーカーは120秒まで同じジョブを処理し続けます。決済や在庫の更新でこれが起きると実害が出ます。タイムアウトはretry_afterより数秒短く、が鉄則です。
例外がSQSです。SQSだけはretry_afterを持たず、AWS側の可視性タイムアウトに従います。設定箇所がLaravelの外へ出るため、コンソール側の値との突き合わせが必要です。SQSそのものの性格はAWS SQS・SNS・SESの違いと使い分け|3サービスの選定基準を比較で整理しています。
failed_jobsの確認と再試行・間引きの運用に使うコマンド
失敗ジョブの操作はArtisanで完結し、手順書には4本を書けば足ります。
php artisan queue:failed
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece
php artisan queue:retry --queue=emails
php artisan queue:retry all
php artisan queue:forget 91401d2c-0784-4f43-824c-34f94a33c24d
php artisan queue:flush --hours=48
queue:failedがジョブID・接続・キュー・失敗時刻の一覧を出し、そのIDをqueue:retryへ渡すと再投入されます。キュー単位やallでのまとめ直しも可能で、原因を直したうえでallを打つのが復旧の基本形です。
放置するとfailed_jobsは際限なく育つので、間引きをスケジューラへ登録しておきます。
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-failed --hours=48')->daily();
既定では24時間より古いレコードが消えます。ジョブをバッチで束ねている場合はjob_batchesテーブルも同様に育つため、queue:prune-batchesも日次で回します。
後始末にはジョブクラスのfailedメソッドを使います。試行上限の超過ならMaxAttemptsExceededException、タイムアウトならTimeoutExceededExceptionが渡るので、例外の型による通知の出し分けが可能です。同期ディスパッチの失敗がfailed_jobsへ入らない点も押さえます。Horizon利用時はhorizon:forgetとhorizon:clearが別にあります。
database・redis・SQSドライバを案件条件で選び分ける
公式ドキュメントは優劣を書きませんが、受託の現場では決め切る必要があります。
databaseで始めてよい条件とRedisへ移す判断の線引き
databaseを選ぶ条件は「アプリサーバが1台から数台」「1分あたりのジョブが数百件まで」「Redisをまだ運用していない」の3つが揃う場合です。追加のミドルウェアなしで動き、バックアップも既存のデータベース運用に乗ります。13系がこれを既定に据えたのは、大半の案件がこの帯に収まるからだと読んでいます。Node.js側で同じ判断をするなら、Redisに加えてPostgreSQLも選べるBullMQのバックエンド選択と採用条件が比較材料になります。
Redisへ移す線引きは、消化の遅れが目に見えたときです。databaseドライバはポーリングで取りに行くため、取得のたびに行ロックが必要です。ワーカーを増やすほど競合が増え、並列数を上げても消化量が伸びない地点が来ます。Redisならblock_forで到着まで待機でき、無駄なポーリングが減ります。
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => 5,
'after_commit' => false,
],
block_forに0を入れるのは避けます。無期限にブロックし、SIGTERMの処理が次のジョブまで遅れるため、デプロイ時の停止が効きません。Redis Clusterではキュー名にキーハッシュタグが要り、queueの値を波括弧で囲みます。serializerとcompressionのRedisオプションがキュードライバで効かない点にも注意します。
SQSを選ぶのはアプリ側で可用性を持ちたくない運用体制のとき
sqsが向くのは、キューの可用性をアプリ側で持ちたくない場合です。冗長化もスケールもAWS側が引き受けます。一方でretry_afterが効かず可視性タイムアウトがAWS側にあること、メッセージサイズの上限、キュー消去に最大60秒の遅延があることは設計時に織り込みます。Horizonも使えません。
13系で加わった接続も、条件が合えば手数を減らせます。deferredはHTTPレスポンス送出後に同じプロセスで、backgroundは別のPHPプロセスを起こして処理する接続です。どちらも常駐が要らず、軽い後処理を逃がせます。failoverは接続を配列で並べ、先頭が使えなければ次へ倒します。
'failover' => [
'driver' => 'failover',
'connections' => [
'redis',
'database',
'sync',
],
],
Redisが落ちたらデータベースへ、それも駄目なら同期実行へ、と逃がせます。ジョブを失いたくない処理では有効な備えでしょう。
キューの導入を見送るべき場面と同期処理へ切り分ける判断の目安
逆に、キューを入れないほうがよい場面もあります。1つ目は1秒未満で終わり、失敗しても画面でやり直せる処理です。ジョブ化するとテーブルとワーカーとデプロイ手順が増え、得るものより運用の手間が上回ります。2つ目は実行順序が厳密に決まっている処理です。順序保証が要るならキュー名を分けてワーカーを1本に絞るか、同期実行のままにします。3つ目はワーカーを常駐させる置き場が無い構成で、共有レンタルサーバのような環境ではdeferredやbackgroundで足りる範囲に収めるほうが早道でしょう。
3つ目に当たる場合、判断はアプリ側ではなくインフラ側の設計へ移ります。ワーカーを常駐させるサーバ構成、RedisやSQSの選定、supervisorとデプロイ手順の整備まで引き受ける体制が要るなら、インフラ構築(AWS・Google Cloud・Azure)で現行構成の前提から相談いただけます。
よくある質問
キューに積んだジョブが処理されないのはなぜですか?
多くはワーカーが動いていないことが原因です。queue:workのプロセスが生きているかsupervisorのstatusで確認します。動いているのに消化されないなら、ワーカーが見ているキュー名と積んだ先が食い違っている可能性が濃厚です。QUEUE_CONNECTIONがsyncのままなら、そもそも積まれずその場で実行されています。
再試行の設定を書いたのに1回しか実行されないのはなぜですか?
queue:workに試行回数を渡していない場合、既定は1回です。ジョブクラス側のTries属性があればその回数が使われますが、どちらも無ければ1回でfailed_jobsへ入ります。supervisorのcommand行に指定があるか確認してください。
コードを直したのに古い挙動のまま動き続けるのはなぜですか?
ワーカーが起動時のコードをメモリへ保持し続けているためです。デプロイ手順にqueue:restartを組み込み、supervisorに再起動させてください。開発中はローカルだけqueue:listenへ切り替える手もあります。
同じジョブが2回実行されてしまう原因は何ですか?
ワーカーのタイムアウトがretry_afterより長くないか確認します。前後が逆だと、再投入されたあとも元のワーカーが処理を続けるため、二重実行の原因です。SQSではAWS側の可視性タイムアウトが同じ役割を持つので、コンソールの値と突き合わせます。冪等な作りにしておくとより安全でしょう。
Horizonとsupervisorはどちらかがあればよいのですか?
役割が違うので置き換えにはなりません。supervisorはプロセスを見張る仕組み、Horizonはワーカー構成の管理と可視化の仕組みです。Horizonを使う場合もhorizonコマンド自体をsupervisorで常駐させます。Redis接続が前提な点にも注意します。
関連記事
- 非同期処理とは?同期処理との違いから実装方式まで実装者目線で解説
- PHPの非同期処理とは|Fibers・ReactPHP・AMPHP・Swooleの違いと実装方法
- Laravel Octaneとは|ワーカー常駐の仕組みと状態リーク・採用条件
- Laravel Forgeとは|自動化される範囲・料金・Cloudとの選択基準
- LaravelでPrometheus+Grafana監視を実装|メトリクス収集・可視化・アラート設定
- AWS SQS・SNS・SESの違いと使い分け|3サービスの選定基準を比較
- Valkeyとは?Redisとの違い・導入方法・データ型を解説【2026年版】
- Laravel Sailとは|sail up/downコマンドの意味と使い方・導入手順