Laravel Queue очереди спасают в тот момент, когда запрос пользователя не должен ждать, пока отработает медленная операция - отправка письма, запрос к СДЭК, генерация PDF или вызов внешнего API оплаты. Я выношу в очередь любую задачу, которая не обязана выполниться прямо сейчас, и за несколько лет работы с Laravel это, пожалуй, один из немногих механизмов фреймворка, который использую в каждом втором проекте с бэкендом.
Что такое очереди Laravel Queue и зачем они нужны
Без очереди контроллер делает всё синхронно: принял запрос, обратился к внешнему сервису, дождался ответа, вернул результат пользователю. Если внешний сервис отвечает 3-5 секунд - а СДЭК и банковские эквайринги любят так отвечать под нагрузкой - пользователь всё это время смотрит на крутящийся спиннер, а PHP-воркер занят одним запросом вместо того, чтобы обрабатывать следующие.
Очередь разрывает эту цепочку. Контроллер создаёт задачу (Job), кладёт её в очередь и сразу отвечает пользователю. Отдельный процесс - воркер - забирает задачи из очереди и выполняет их в фоне, независимо от HTTP-запроса. На практике я выношу в очередь:
- отправку писем и push-уведомлений после регистрации или заказа;
- обработку вебхуков от платёжных систем вроде T‑Bank - сам вебхук нужно подтвердить за доли секунды, а логику начисления заказа можно обработать через пару секунд;
- создание заказа и получение трек-номера в СДЭК после оплаты;
- рассылку сообщений через Telegram-бота большому списку пользователей;
- генерацию отчётов, PDF-счетов, экспортов в Excel;
- синхронизацию данных с CRM или сторонним API.
Общее правило простое: если задача занимает больше 200-300 мс или зависит от внешнего сервиса, который может залагать или упасть, - это кандидат в очередь.
Драйверы очередей Laravel: database, Redis, SQS и что выбрать
Laravel из коробки поддерживает несколько драйверов, и выбор между ними - это выбор между простотой и производительностью.
| Драйвер | Нужна доп. инфраструктура | Скорость | Когда беру |
|---|---|---|---|
| sync | нет | задачи выполняются мгновенно, в том же запросе | только для локальной разработки и тестов |
| database | нет, использует MySQL/PostgreSQL | средняя, до пары сотен задач в секунду | небольшие и средние проекты, когда не хочется поднимать Redis ради очереди |
| redis | да, нужен Redis-сервер | высокая | проекты с нагрузкой, где важна скорость обработки и нужны приоритеты очередей |
| Amazon SQS | да, облачный сервис AWS | высокая, с автомасштабированием | инфраструктура уже в AWS, нужна отказоустойчивость без своего сервера очередей |
| beanstalkd | да, отдельный демон | высокая | встречается редко, обычно в legacy-проектах |
На старте почти всегда ставлю database - он не требует ничего, кроме миграции, и для 90% проектов с интернет-магазином или CRM его хватает с запасом. На Redis перехожу, когда в очереди больше пары тысяч задач в день или нужны отложенные задачи (delayed jobs) с точным временем выполнения - database-драйвер тоже это умеет, но Redis делает это заметно быстрее под нагрузкой.
Настройка очереди Laravel Queue с нуля
Для database-драйвера порядок действий такой:
php artisan queue:table
php artisan migrate
В .env указываю драйвер:
QUEUE_CONNECTION=database
Дальше создаю класс задачи:
php artisan make:job SendOrderToSdek
Внутри job передаю данные через конструктор и описываю логику в методе handle:
class SendOrderToSdek implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $backoff = 30;
public function __construct(public Order $order) {}
public function handle(SdekService $sdek): void
{
$sdek->createShipment($this->order);
}
}
Запускаю задачу из контроллера или события одной строкой:
SendOrderToSdek::dispatch($order);
Важный момент, который упускают новички: без запущенного воркера задачи просто копятся в таблице jobs и не выполняются. Очередь - это не магия, а таблица (или список в Redis) плюс отдельный процесс, который её вычитывает.
Бесплатный материал
🎁 Полезный скрипт в подарок
Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.
Без спама. Отписка в 1 клик.
Supervisor и запуск воркеров в продакшене
Локально воркер можно запустить командой:
php artisan queue:work --tries=3 --timeout=90
Но в проде этот процесс должен работать постоянно и перезапускаться при падении или после каждого деплоя - код воркера кешируется в памяти PHP, и без перезапуска он продолжит работать со старой версией классов. Для этого использую Supervisor:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/site/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/site/storage/logs/worker.log
stopwaitsecs=3600
Параметр numprocs=2 запускает два параллельных воркера - это уже даёт параллельную обработку задач без лишней сложности. После каждого деплоя обязательно выполняю php artisan queue:restart - команда мягко останавливает воркеры после текущей задачи, а Supervisor их тут же поднимает заново с новым кодом. У меня в готовых конфигурациях для проектов на Laravel есть отработанный шаблон Supervisor-конфига для очередей, который экономит время на первой настройке сервера.
Важно не путать queue:work и queue:listen - listen перезагружает приложение на каждую задачу и удобен для разработки, но в проде это лишний расход ресурсов. work держит приложение в памяти и работает быстрее, но требует restart после деплоя.
Retry, failed jobs и мониторинг фоновых задач
Задачи падают - эквайринг вернёт 500‑ю ошибку, СДЭК уйдёт в даунтайм на техобслуживание, стороннее API отдаст таймаут. Laravel сохраняет упавшие задачи в таблицу failed_jobs, если она создана:
php artisan queue:failed-table
php artisan migrate
Посмотреть упавшие задачи и перезапустить их вручную:
php artisan queue:failed
php artisan queue:retry all
В самом Job классе задаю количество попыток и задержку между ними через $tries и $backoff - для интеграции с СДЭК обычно ставлю 3 попытки с нарастающей паузой в 30, 60 и 120 секунд, потому что мгновенный повтор при перегрузке стороннего API почти всегда упадёт с той же ошибкой. Метод failed() в классе задачи даёт возможность отправить уведомление в Telegram-бота на aiogram или в свой канал мониторинга, если задача не выполнилась после всех попыток:
public function failed(Throwable $exception): void
{
Notification::route('telegram', config('services.telegram.chat_id'))
->notify(new JobFailedNotification($this->order, $exception));
}
Для проектов на Redis-драйвере ставлю Laravel Horizon - он даёт дашборд с метриками по очередям, throughput и временем выполнения задач в реальном времени, плюс удобное управление приоритетами очередей через конфиг вместо ручного распределения воркеров.
Когда очереди нужны, а когда это лишняя сложность
Очередь - это ещё один движущийся компонент системы: отдельный процесс, который может упасть, застрять или не подхватить деплой. Для лендинга с формой обратной связи или простого сайта на Tilda с кастомным скриптом очередь не нужна - там задача одна и выполняется за 100-200 мс синхронно.
Я ставлю очередь, когда выполняется хотя бы одно из условий:
- операция зависит от внешнего API, который может отвечать медленно или падать;
- задачу нужно повторить при ошибке без участия пользователя;
- объём работы разовый, но большой - рассылка по 5000 подписчикам разом;
- несколько независимых действий можно выполнить параллельно вместо последовательного ожидания.
Есть и альтернатива очередям в коде - вынести асинхронную логику в n8n, если сценарий скорее про интеграцию сервисов между собой (пришёл заказ - создать задачу в CRM - отправить письмо - уведомить в Telegram), а не про бизнес-логику приложения. Я обычно провожу границу так: если асинхронная задача тесно завязана на модели и логику самого приложения - это Job в очереди Laravel; если это склейка нескольких внешних сервисов без сложной логики - это кандидат на n8n-сценарий, там же проще редактировать порядок шагов без деплоя кода.
Для проектов, где очереди нужны с нуля - новый бэкенд на Laravel с интеграциями оплаты и доставки, я закладываю такую архитектуру сразу в разработку, а не добавляю задним числом: переписывать синхронные контроллеры под очереди на живом проекте с данными в проде выходит дороже и рискованнее, чем сделать правильно с первой итерации.
API и серверная часть под SPA
API / Бэкенд
от 100 000 ₽
Подробнее →Частые вопросы
Чем очередь отличается от cron-задачи в Laravel?
Cron (Task Scheduling в Laravel) запускает задачу по расписанию - раз в час, раз в сутки, независимо от того, произошло ли какое-то событие. Очередь реагирует на конкретное действие в приложении и выполняется как можно быстрее после того, как задача в неё попала. Часто используются вместе: cron раз в 5 минут ставит в очередь пачку задач на отправку писем, а воркеры их разбирают.
Можно ли использовать несколько очередей одновременно?
Да, и я почти всегда так делаю на проектах с разной приоритетностью задач. Например, очередь high для отправки данных в платёжный шлюз и обработки вебхуков, default для писем и уведомлений, low для отчётов и экспортов. Воркер запускается с указанием порядка: php artisan queue:work --queue=high,default,low - так критичные задачи не застревают за длинными фоновыми отчётами.
Что будет, если воркер упадёт во время обработки задачи?
Зависит от драйвера и настроек. У database и Redis есть механизм timeout - если воркер не подтвердил выполнение задачи за отведённое время (по умолчанию 60 секунд), задача считается зависшей и возвращается в очередь для повторной обработки. Поэтому важно задавать идемпотентную логику в handle() - при повторном запуске задача не должна создавать дубль заказа или письма, я обычно добавляю проверку по уникальному идентификатору перед выполнением действия.
Нужен ли Redis для очередей или хватит database-драйвера?
Для старта и среднего объёма задач - до нескольких тысяч в день - database-драйвера достаточно, он не требует отдельного сервера и настраивается за 5 минут. Redis имеет смысл подключать, когда очередь становится узким местом: задачи копятся быстрее, чем воркеры успевают их разбирать, либо нужны отложенные задачи с точным временем и высокая скорость записи под пиковую нагрузку, например в дни распродаж на интернет-магазине.