Очереди в Laravel: как делать правильно и не терять задачи
Джобы падают молча, воркеры едят память, отправка письма блокирует ответ пользователю. Разбираем рабочие приёмы работы с очередями в Laravel 11: драйверы, retry, идемпотентность и деплой без потерь.

Пользователь жмёт «Оформить заказ», а Laravel в это время синхронно шлёт письмо через SMTP, дёргает платёжный шлюз и генерирует PDF. Ответ приходит через восемь секунд, часть пользователей уходит. Очереди решают ровно эту задачу: тяжёлую работу вынести в фон, а пользователю отдать ответ сразу. Проблема в том, что «просто добавить ShouldQueue» — это начало, а не конец. Дальше начинаются потерянные задачи, дубли платежей и воркеры, которые тихо умерли ночью. Разберём, как настроить очереди в Laravel 11 так, чтобы они не создавали новых проблем.
Статья про рабочие приёмы: какой драйвер выбрать, как настроить повторы, как не отправить письмо дважды и как деплоить без потери задач. Внутреннее устройство драйверов и Horizon под высокой нагрузкой — отдельная тема, здесь не разворачиваем.
Выбор драйвера: не начинайте с database навсегда
Laravel из коробки поддерживает несколько драйверов очереди, они настраиваются в config/queue.php и переменной QUEUE_CONNECTION в .env. По умолчанию в свежем проекте стоит database — очередь хранится в таблице jobs. Это удобно для старта: не нужен отдельный сервис. Но у каждого драйвера своя цена.
| Драйвер | Плюс | Цена | Когда брать |
|---|---|---|---|
| sync | Ничего не настраивать, задача выполняется сразу | Это не фон, всё блокирует запрос | Только локальная разработка и тесты |
| database | Не нужен отдельный сервис, задачи видно в таблице | Блокировки строк, лишняя нагрузка на БД под потоком задач | Небольшие проекты, десятки задач в минуту |
| redis | Быстрый, поддерживает Horizon, отложенные задачи | Нужен Redis, при падении без persistence теряете очередь | Основной выбор для боевых проектов |
| sqs | Управляемый AWS, масштабируется сам | Задержки видимости, лимит на размер сообщения, привязка к облаку | Инфраструктура уже в AWS |
Практический совет: на старте живите на database, но не завязывайтесь на его особенности. Как только задач становится больше нескольких в секунду или появляется потребность в Horizon — переезжайте на redis. Смена драйвера при аккуратном коде — это правка .env и перезапуск воркеров.
Джоба, которую не страшно повторить
Главное свойство очереди: задача может выполниться повторно. Воркер упал в середине, таймаут сработал, Redis мигнул — и одна и та же джоба уйдёт в обработку снова. Если джоба списывает деньги или шлёт письмо, повтор превращается в двойное списание и второе письмо клиенту.
Отсюда правило: джоба должна быть идемпотентной. Один и тот же вход даёт один и тот же результат независимо от числа запусков. Проверяйте состояние перед действием, а не действуйте вслепую.
<?php
// app/Jobs/ChargeOrder.php — Laravel 11
namespace App\Jobs;
use App\Models\Order;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ChargeOrder implements ShouldQueue
{
use Queueable;
// Сколько раз пробовать до провала
public int $tries = 3;
// Сколько секунд ждать между попытками (нарастающий backoff)
public array $backoff = [10, 30, 60];
public function __construct(public int $orderId) {}
public function handle(): void
{
$order = Order::findOrFail($this->orderId);
// Идемпотентность: если уже оплачено — выходим без действий
if ($order->isPaid()) {
return;
}
$order->charge();
}
}Передавайте в конструктор идентификатор, а не саму модель целиком: Laravel сериализует джобу для хранения в очереди, и распухший объект — лишние байты и риск работать со старым состоянием. С Eloquent-моделями трейт SerializesModels (входит в Queueable) сам хранит только ключ и подтягивает свежую запись при выполнении — но явный $orderId нагляднее и надёжнее.
Повторы, таймауты и провалы
Три параметра управляют поведением при сбое, и путать их — частая ошибка.
- tries — сколько раз пробовать выполнить джобу. По умолчанию 1, то есть без повторов вообще. Ставьте осознанно.
- backoff — пауза между попытками. Массив задаёт нарастающую задержку: не долбите упавший внешний сервис каждую секунду.
- timeout — сколько секунд джобе разрешено выполняться, прежде чем воркер её прибьёт. Должен быть меньше, чем
retry_afterв конфиге connection, иначе задачу заберут в работу дважды параллельно.
Соотношение timeout и retry_after — источник тихих дублей. Если джоба работает 90 секунд, а retry_after стоит 90, очередь сочтёт её зависшей и выдаст второму воркеру, пока первый ещё работает. Держите retry_after заведомо больше самого долгого timeout.
Когда попытки исчерпаны, джоба уходит в таблицу failed_jobs (её создаёт миграция при установке). Обработайте провал явно — верните заказ в статус ошибки, уведомите команду:
// тот же класс ChargeOrder
public function failed(\Throwable $e): void
{
Order::find($this->orderId)?->markChargeFailed($e->getMessage());
}Смотреть упавшие задачи и перезапускать — командами Artisan:
# посмотреть список
php artisan queue:failed
# перезапустить одну по id
php artisan queue:retry 5
# перезапустить все
php artisan queue:retry allОчереди по приоритету
Не все задачи равны. Письмо о сбросе пароля должно уйти за секунды, а ночной пересчёт статистики подождёт. Разведите их по именованным очередям и запускайте воркер с приоритетом.
// диспатч в конкретную очередь
ProcessPasswordReset::dispatch($user)->onQueue('high');
RecalculateStats::dispatch()->onQueue('low');# воркер разбирает сначала high, потом low
php artisan queue:work redis --queue=high,lowПорядок в --queue важен: воркер полностью выгребает high, прежде чем возьмётся за low. Если критичных задач много, поднимайте отдельные воркеры под high, чтобы тяжёлая low не заняла все процессы.
Деплой без потери задач
Воркер — это долгоживущий PHP-процесс. Он загружает код один раз при старте и держит его в памяти. Значит, после деплоя старый воркер продолжает крутить старую версию кода, пока его не перезапустят. Это ловушка: вы выкатили исправление, а джобы бегут по старой логике.
- После каждого деплоя выполняйте
php artisan queue:restart. Команда мягко сигналит воркерам завершиться после текущей задачи, супервизор поднимет их заново с новым кодом. - Не убивайте воркеры
kill -9— задача прервётся посередине и, если она не идемпотентна, оставит систему в грязном состоянии. - Держите воркеры под супервизором (Supervisor или Horizon), а не запускайте руками в терминале — иначе после разрыва SSH-сессии они умрут.
Минимальный конфиг Supervisor для воркера выглядит так:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=4
stopwaitsecs=3600Флаг --max-time=3600 перезапускает воркер раз в час, чтобы отпустить накопленную память — PHP-процессы имеют свойство пухнуть. Параметр stopwaitsecs даёт джобе доработать при остановке, не обрезая её.
Частые ошибки, которые ловят в проде
- Забыли запустить воркер. Джобы копятся в таблице
jobs, ничего не выполняется, а логи молчат. Проверьте, что процессqueue:workвообще жив. - tries=1 по умолчанию. Первый же сетевой сбой отправляет задачу в failed навсегда. Задавайте
triesявно. - Передали в джобу большой объект или замыкание. Замыкания не сериализуются, объект раздувает очередь. Передавайте скаляры и id.
- Не перезапустили воркер после деплоя. Старый код обрабатывает новые задачи. Впишите
queue:restartв скрипт деплоя. - Джоба не идемпотентна. Повтор шлёт второе письмо или списывает деньги дважды. Проверяйте состояние перед действием.
Что настроить дальше
Когда база очередей работает, следующий шаг — наблюдаемость. На redis ставьте Laravel Horizon: он даёт дашборд с метриками пропускной способности, временем выполнения и списком провалов, а также балансировку воркеров по очередям. Настройте алерт на рост таблицы failed_jobs и на длину очереди — эти два показателя первыми сигналят, что воркеров не хватает или что-то падает системно. Следующая тема для изучения — батчи и цепочки джоб (Bus::batch, Bus::chain) для составных фоновых процессов.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: Laravel 11 — Queues (официальная документация), Laravel Horizon
Частые вопросы
У меня Laravel 10, а не 11. Код подойдёт?
Джобы просто не выполняются, ошибок нет. В чём дело?
Как избежать двойного списания, если джоба выполнилась дважды?
Что делать с задачами в failed_jobs?
Нужно ли перезапускать воркер после каждого деплоя?
Почему воркер съедает всё больше памяти?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.