Настройка Laravel Queue - первое, что я делаю в проекте, как только в коде появляется отправка писем, генерация PDF или приём вебхуков от Т‑Банка: без очередей такие операции выполняются прямо в HTTP-запросе и упираются в таймаут php-fpm. Пользователь оформил заказ, а сервер в это время генерирует накладную для СДЭК, шлёт письмо и дёргает вебхук в n8n - запрос виснет на 5-8 секунд, а на слабом хостинге падает по 30-секундному лимиту. Очередь выносит всё это в фоновый процесс: клиент получает ответ мгновенно, а воркер обрабатывает задачу отдельно.
По теме статьи
Готовое решение
AI-чатбот для сайта на Claude - отвечает как ваш менеджер, работает 24/7
Подключу к вашему сайту чат-бота на Claude API. Бот отвечает на вопросы клиентов голосом вашего бренда, знает каталог и условия доставки, забирает лиды в CRM или Telegram.
от25 000 ₽
API / Бэкенд
API и серверная часть под SPA
REST API на Laravel для React/Vue/мобильного приложения. Аутентификация, права, бизнес-логика, очереди задач.
от100 000 ₽
Зачем нужны очереди и когда без них не обойтись
Правило простое: если операция дольше 200-300 мс или зависит от внешнего API, её место в очереди. На практике это отправка email и SMS, генерация PDF-счетов и отчётов, обработка загруженных изображений, синхронизация заказов с 1С или CRM, приём вебхуков от платёжных систем и запросы к внешним сервисам вроде расчёта доставки СДЭК. У меня был кейс с интернет-магазином на Laravel и приёмом оплат через Т‑Банк: эквайринг присылал уведомление об оплате, а обработчик сразу пытался сформировать чек через ОФД, обновить остатки и отправить письмо клиентом - всё синхронно. При скачке нагрузки вебхук от банка не укладывался в таймаут, банк слал повтор, и заказ обрабатывался дважды. Перенос логики в job с уникальным ключом идемпотентности снял проблему полностью.
Вторая причина - устойчивость к сбоям. Если внешний сервис (СДЭК, ОФД, SMS-шлюз) недоступен, синхронный запрос просто падает с ошибкой у пользователя. Job из очереди можно ретраить с задержкой, логировать в failed_jobs и разбирать руками, не трогая основной поток заказа.
Настройка драйверов очереди в config/queue.php
Laravel из коробки умеет работать с несколькими драйверами: sync (для локальной отладки, задачи выполняются сразу), database, redis, а также beanstalkd и Amazon SQS. Для боевого проекта я выбираю между database и redis.
| Драйвер | Где хранятся задачи | Когда использую |
|---|---|---|
| sync | нигде, выполняется сразу | только локальная разработка и тесты |
| database | таблица jobs в MySQL/PostgreSQL | небольшой проект, нет отдельного Redis, нагрузка низкая |
| redis | списки и хэши в Redis | средняя и высокая нагрузка, нужны приоритеты и задержки |
| sqs | Amazon SQS | инфраструктура уже в AWS, нужна масштабируемость без своего сервера очередей |
Для database-драйвера сначала генерирую таблицы миграций:
php artisan queue:table
php artisan queue:failed-table
php artisan migrate
В .env указываю драйвер и подключение:
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_QUEUE=default
Database-драйвер проще в развёртывании - не нужен отдельный сервис, но каждая проверка новой задачи это SELECT с блокировкой строки, и на потоке в несколько сотен задач в минуту база начинает тормозить. Redis держит такую нагрузку без проблем и даёт приоритеты очередей, задержки (delay) и явный контроль ретраев без лишних запросов к БД.
Как создать job и запустить воркер через artisan queue:work
Задача создаётся командой php artisan make:job ProcessOrderPayment. Класс реализует интерфейс ShouldQueue, а вся логика уходит в метод handle:
class ProcessOrderPayment implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $timeout = 90;
public $backoff = [10, 30, 60];
public function __construct(public Order $order) {}
public function handle(CdekService $cdek): void
{
$this->order->markAsPaid();
$cdek->registerShipment($this->order);
Mail::to($this->order->email)->send(new OrderPaidMail($this->order));
}
public function failed(Throwable $exception): void
{
Log::error('Order payment job failed', ['order_id' => $this->order->id]);
}
}
Отправка в очередь: ProcessOrderPayment::dispatch($order). Дальше запускаю воркер, который забирает задачи и выполняет handle:
php artisan queue:work redis --queue=payments,default --tries=3 --timeout=90 --sleep=1
Параметр - queue задаёт приоритет: воркер сначала выгребет всё из payments и только потом перейдёт к default. Отдельные очереди под критичные задачи (оплата, вебхуки) и отдельные под второстепенные (аналитика, логирование) - это то, что почти всегда упускают на старте и потом переделывают, когда письмо с чеком встаёт в очередь после тысячи задач по обновлению кэша каталога.
Бесплатный материал
🎁 Полезный скрипт в подарок
Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.
Без спама. Отписка в 1 клик.
Supervisor для автозапуска и перезапуска воркеров
Команда queue:work живёт, пока не упадёт процесс или не кончится память - PHP-воркер держит один и тот же процесс часами, и это как раз то место, где утечки памяти в коде вылезают в первую очередь. Без супервизора процесса воркер после падения просто перестанет забирать задачи, и очередь будет копиться молча.
На Ubuntu/Debian ставлю Supervisor (apt install supervisor) и создаю конфиг в /etc/supervisor/conf.d/laravel-worker.conf:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --queue=payments,default --sleep=1 --tries=3 --timeout=90 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=3
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log
stopwaitsecs=3600
numprocs=3 запускает три параллельных воркера - это число подбираю под нагрузку, а не ставлю наугад: для проекта с редкими письмами хватает одного, для магазина с оплатами и вебхуками в пик обычно беру 3-4 процесса и слежу за очередью через мониторинг. max-time=3600 заставляет воркер перезапускаться раз в час сам по себе, это подстраховка от медленных утечек памяти, которые не успевают вылезти за час работы. После правки конфига:
supervisorctl reread
supervisorctl update
supervisorctl start laravel-worker:*
Отдельный момент - деплой. Воркер держит код в памяти на момент старта, поэтому после каждого деплоя новую версию кода он не увидит, пока процесс не перезапустят. В CI/CD после выкладки кода всегда добавляю php artisan queue:restart - команда не убивает процессы принудительно, а ставит флаг, и воркер завершает текущую задачу и перезапускается сам, Supervisor поднимает его заново с новым кодом.
Laravel Horizon для мониторинга очередей на Redis
Eсли драйвер очереди - Redis, ставлю Horizon (composer require laravel/horizon). Он даёт дашборд с графиками пропускной способности, списком упавших задач, временем выполнения по каждому job и удобным ретраем прямо из интерфейса вместо ковыряния в failed_jobs через tinker.
В config/horizon.php задаю баланс воркеров под окружение:
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['payments', 'default'],
'balance' => 'auto',
'minProcesses' => 2,
'maxProcesses' => 8,
'tries' => 3,
],
],
balance: auto сам поднимает и опускает число процессов в зависимости от длины очереди - удобно, когда нагрузка скачет (распродажа, рассылка по базе), и не хочется вручную считать numprocs как в Supervisor. Сам Horizon тоже запускается через Supervisor одной строкой: php artisan horizon вместо queue:work. Дашборд по умолчанию доступен только в local-окружении, для продакшена доступ ограничиваю через Gate в HorizonServiceProvider, иначе список задач с содержимым payload увидит кто угодно, кто угадает URL.
Если очередь на database-драйвере, Horizon не подключить - для неё смотрю на failed_jobs и на длину очереди через php artisan queue:monitor database:default --max=100, которая пишет в лог и может слать уведомление, если задач скопилось больше порога.
Частые ошибки при настройке воркеров под фоновые задачи
За несколько лет работы с Laravel на разных проектах чаще всего натыкаюсь на одни и те же грабли:
- Забыли queue:restart после деплоя - воркер месяцами работает на старой версии кода, баг вроде бы поправлен в репозитории, а в проде всё ещё падает.
- Не задан timeout - тяжёлый job (например, генерация большого PDF-отчёта) виснет и держит воркер часами, а остальные задачи в очереди просто ждут.
- Один воркер на все очереди без приоритета - критичная задача (подтверждение оплаты) стоит в общей куче после тысячи фоновых обновлений кэша.
- Нет обработки failed_jobs - задачи падают молча, узнают об этом только когда клиент напишет, что не пришло письмо с чеком.
- Слишком много numprocs на слабом сервере - три воркера одновременно упираются в память и CPU, обычный HTTP-трафик сайта начинает тормозить.
Если нужно настроить очереди в существующем проекте или спроектировать фоновую обработку задач с нуля под конкретную нагрузку, обычно это делается в рамках разработки API и бэкенда на Laravel - там я закладываю правильную архитектуру очередей сразу, а не переделываю её после первого падения сервера под нагрузкой.
Частые вопросы
Сколько воркеров нужно запускать для проекта на Laravel?
Зависит от объёма фоновых задач и ресурсов сервера. Для небольшого проекта с редкими письмами хватает одного процесса. Для интернет-магазина с оплатами, доставкой и рассылками обычно ставлю 2-4 воркера с разделением по очередям через параметр - queue, а точное число подбираю по факту, глядя на длину очереди в Horizon или через queue:monitor.
Что делать, если очередь Laravel зависла и задачи не выполняются?
Сначала проверяю, жив ли сам процесс воркера: supervisorctl status laravel-worker:*. Если процесс жив, но задачи не двигаются - смотрю на timeout, возможно один job завис на внешнем API и держит воркер. Если процесс упал, Supervisor должен поднять его сам благодаря autorestart=true; если конфиг без Supervisor, значит воркер просто умер после ошибки и нужно перезапускать вручную.
Нужен ли Horizon, если очередь работает на database, а не на Redis?
Нет, Horizon работает только с Redis-подключением. Для database-драйвера мониторю очередь командой queue:monitor и таблицей failed_jobs напрямую. Если проект растёт и очередь начинает нагружать основную базу данных, это обычно сигнал переезжать на Redis и подключать Horizon.
Как перезапустить воркеры после деплоя без потери задач?
Командой php artisan queue:restart. Она не убивает процесс мгновенно - ставит сигнал в кэш, и воркер завершает текущую задачу до конца, только после этого останавливается. Supervisor видит остановку и поднимает процесс заново уже с обновлённым кодом, задачи из очереди при этом не теряются.