Очередь нужна не только большим проектам. Отправка письма, генерация PDF, обращение к чужому API — всё это может длиться секунды и держать запрос пользователя. Проще положить задачу в очередь и ответить браузеру сразу. Для этого не обязателен Redis: Laravel умеет хранить задачи в обычной таблице MySQL.
Зачем очередь, если сайт маленький
Типичный сценарий: форма отправляет письмо через SMTP. Без очереди пользователь ждёт, пока почтовый сервер ответит, а при таймауте видит ошибку, хотя данные уже сохранены. С очередью запись в базу занимает миллисекунды, письмо уходит в фоне, а сбой можно повторить.
Второй сценарий — тяжёлые задачи: импорт CSV, обход внешнего API с лимитами, пересчёт статистики. Их нельзя выполнять внутри HTTP-запроса: PHP-процесс ограничен по времени, а при обрыве соединения работа теряется.
Таблицы jobs и failed_jobs
Драйвер database хранит задания в двух таблицах. Создаются они штатными командами:
php8.3 artisan queue:table
php8.3 artisan queue:failed-table
php8.3 artisan migrate
В .env укажите соединение:
QUEUE_CONNECTION=database
После правки .env на сервере нужен php8.3 artisan config:clear, иначе Laravel продолжит читать закешированный конфиг. Таблица failed_jobs появляется не всегда: без неё упавшее задание просто исчезнет, и вы не узнаете причину.
Как поставить задачу в очередь
Задание — обычный класс с трейтом ShouldQueue. Внутри — то, что раньше выполнялось в контроллере:
namespace App\Jobs;
use App\Models\Order;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class SendOrderMail implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public int $timeout = 60;
public function __construct(public Order $order) {}
public function handle(): void
{
// отправка письма
}
}
Постановка в очередь выглядит короче, чем кажется:
SendOrderMail::dispatch($order);
SendOrderMail::dispatch($order)->delay(now()->addMinutes(10));
Модель в конструкторе сериализуется по идентификатору. Если запись удалят до запуска, Laravel выбросит исключение — это нормальное поведение, а не ошибка очереди.
Запуск воркера на shared-хостинге
На VPS воркер держит Supervisor: процесс живёт постоянно и перезапускается при падении. На виртуальном хостинге Supervisor обычно недоступен, зато есть cron. Рабочая схема — запускать воркер раз в минуту с ограничением по времени:
* * * * * php8.3 /home/USER/site.ru/artisan queue:work --stop-when-empty --max-time=50 >> /home/USER/queue.log 2>&1
--stop-when-empty завершает процесс, когда задания кончились, --max-time не даёт ему работать дольше минуты и накладываться на следующий запуск. Пути должны быть абсолютными: cron не наследует рабочий каталог проекта.
Есть и второй вариант — queue:listen, который перезапускает обработчик после каждого задания. Он удобнее при разработке, потому что подхватывает изменения в коде, но нагружает сервер сильнее. Для cron-схемы берите queue:work.
Если задачи должны запускаться по расписанию, а не сразу, их удобнее описывать в планировщике — об этом отдельная статья про планировщик Laravel и cron. А проверить само cron-выражение можно в генераторе cron.
retry_after, timeout и дубли задач
Два параметра легко перепутать. timeout — сколько секунд живёт обработчик, после чего его убивают. retry_after — через сколько секунд задача считается потерянной и возвращается в очередь. Если retry_after меньше timeout, вторую копию выдадут, пока первая ещё работает.
Правило простое: retry_after должен быть заметно больше максимального времени выполнения. Значения задаются в config/queue.php и в переменных окружения. Заодно проверьте, что задача идемпотентна: повторная отправка письма или повторное списание денег — плохая новость.
Что делать с упавшими задачами
Посмотреть список и причину:
php8.3 artisan queue:failed
php8.3 artisan queue:retry all
php8.3 artisan queue:flush
queue:retry all вернёт задания в работу, queue:flush удалит их навсегда. Перед массовым повтором убедитесь, что причина устранена: иначе получите тот же сбой десять раз подряд. Для критичных задач добавьте уведомление в Telegram при событии JobFailed.
Когда всё-таки нужен Redis
Database-драйвер блокирует строку таблицы при выборке задания. При десятках воркеров и тысячах задач в минуту это становится узким местом. Redis решает вопрос скорости и умеет отложенные задачи эффективнее. Но если у вас один сервер и несколько сотен заданий в сутки, разница незаметна, а лишняя зависимость только усложняет деплой.
Коротко
QUEUE_CONNECTION=databaseи две миграции — минимальный набор;- воркер запускается из cron с
--stop-when-empty --max-time; retry_afterбольшеtimeout, иначе будут дубли;failed_jobsнужна, чтобы видеть причины сбоев;- Redis оправдан при высокой нагрузке, а не «на всякий случай».