На shared-хостинге обычно нельзя установить Supervisor или systemd для постоянно работающего worker-а. Это не мешает использовать очереди Laravel: jobs хранятся в базе данных, а короткий worker запускается cron-ом каждую минуту.
Подход подходит для Beget и других shared-хостингов, не требует Redis и хорошо работает для писем, уведомлений, Telegram-рассылок, импорта данных и других фоновых задач.
Главный принцип: HTTP-запрос только ставит работу в очередь. Обработку выполняет отдельный PHP-процесс через cron.
Что потребуется
- Laravel с доступом к
php artisan; - cron в панели хостинга;
- MySQL или PostgreSQL;
- PHP CLI той же версии, что и у сайта;
- права на выполнение миграций и запись логов.
На Beget путь к PHP CLI обычно отображается при создании cron-задачи. Не подставляйте путь наугад: используйте путь, который показывает панель хостинга.
1. Включите database queue
В production .env задайте:
QUEUE_CONNECTION=database
CACHE_STORE=database
DB_QUEUE_RETRY_AFTER=300
Затем очистите cache конфигурации:
php artisan optimize:clear
Почему не sync
При такой конфигурации:
QUEUE_CONNECTION=sync
вызов dispatch() запускает job сразу внутри текущего HTTP/AJAX-запроса. Это плохо для долгих действий: пользователь ждёт ответ, PHP может получить timeout, а повтор запроса способен повторить работу.
database сохраняет job в таблицу jobs. Она будет ждать worker-а и не зависит от времени жизни web-запроса.
2. Создайте таблицы очередей
Если таблицы ещё не созданы, выполните:
php artisan make:queue-table
php artisan make:queue-failed-table
php artisan migrate --force
Laravel создаст:
jobs— ожидающие и зарезервированные jobs;failed_jobs— jobs, исчерпавшие допустимое число попыток.
В уже работающем проекте обычно достаточно применить штатные миграции:
php artisan migrate --force
3. Добавьте cron для Laravel Scheduler
Scheduler выполняет расписание из routes/console.php: например, проверяет наступившие события, создаёт jobs, запускает регулярную очистку и уведомления.
Создайте cron task раз в минуту:
* * * * * flock -n /tmp/laravel-schedule.lock sh -c 'cd /path/to/project && /path/to/php artisan schedule:run' >> /path/to/logs/laravel-schedule.log 2>&1 || true
Замените значения:
/path/to/project— абсолютный путь к корню Laravel-проекта, где находитсяartisan;/path/to/php— абсолютный путь к PHP CLI;/path/to/logs— каталог, доступный для записи. Можно использоватьstorage/logsпроекта.
flock создаёт lock-файл и не допускает одновременного запуска двух schedule:run. Это важно, если одна scheduler-задача работает дольше минуты.
Scheduler не исполняет queue jobs автоматически. Он может только поставить их в
jobs. Для обработки нужен отдельный queue worker.
4. Добавьте cron для worker-а очереди
Worker берёт jobs из jobs и вызывает их handle().
Для основной очереди default добавьте второй cron task, также раз в минуту:
* * * * * flock -n /tmp/laravel-default-queue.lock sh -c 'cd /path/to/project && /path/to/php artisan queue:work database --queue=default --stop-when-empty --max-time=50 --max-jobs=100 --memory=128 --sleep=3 --tries=3 --timeout=60' >> /path/to/logs/laravel-default-queue.log 2>&1 || true
Это не постоянный daemon. Cron запускает короткий worker, тот обрабатывает доступные jobs и завершается. При следующем запуске появляется новый PHP-процесс с чистой памятью.
Значение параметров
| Параметр | Назначение | Рекомендация для cron worker-а |
|---|---|---|
database |
Подключение очереди | Должно совпадать с QUEUE_CONNECTION=database |
--queue=default |
Очередь для обработки | Укажите одну или несколько нужных очередей |
--stop-when-empty |
Завершение при пустой очереди | Обязательно для cron-сценария |
--max-time=50 |
Максимальное время жизни worker-а | Меньше минуты, чтобы cron-процессы не пересекались |
--max-jobs=100 |
Максимум jobs за один запуск | Перезапускает процесс при постоянно растущей очереди |
--memory=128 |
Лимит памяти процесса в МБ | Подберите ниже PHP memory_limit |
--sleep=3 |
Ожидание после пустой очереди | 1–5 секунд обычно достаточно |
--tries=3 |
Число попыток job | Подходит для большинства обычных jobs |
--timeout=60 |
Максимум времени одной job | Должен быть меньше retry_after |
DB_QUEUE_RETRY_AFTER=300 больше --timeout=60, поэтому Laravel не сочтёт ещё выполняемую job брошенной раньше времени.
5. Несколько очередей и приоритеты
У Laravel есть именованные очереди. Их полезно использовать, когда тяжёлые jobs не должны задерживать критичные.
Например:
SendReportJob::dispatch($report)->onQueue('reports');
SendNotificationJob::dispatch($notification)->onQueue('notifications');
Для каждой важной очереди можно завести самостоятельный cron worker.
Пример: критичные уведомления
* * * * * flock -n /tmp/laravel-notifications.lock sh -c 'cd /path/to/project && /path/to/php artisan queue:work database --queue=notifications --stop-when-empty --max-time=50 --max-jobs=100 --memory=128 --sleep=3 --tries=3 --timeout=60' >> /path/to/logs/laravel-notifications.log 2>&1 || true
Пример: тяжёлый импорт
* * * * * flock -n /tmp/laravel-imports.lock sh -c 'cd /path/to/project && /path/to/php artisan queue:work database --queue=imports --stop-when-empty --max-time=50 --max-jobs=10 --memory=192 --sleep=3 --tries=2 --timeout=120' >> /path/to/logs/laravel-imports.log 2>&1 || true
Отдельный worker для notifications или imports не обрабатывает jobs из default. Если используются разные очереди, worker нужен для каждой из них — либо укажите несколько очередей в порядке приоритета:
php artisan queue:work database --queue=notifications,default
На shared-хостинге отдельные короткие worker-ы обычно понятнее и безопаснее: одна проблемная очередь не блокирует другую.
6. Время и память: что worker защищает, а что нет
--max-time, --max-jobs и --memory нужны не для ограничения одной job, а для регулярного перезапуска всего процесса worker-а.
Это защищает от медленного накопления памяти библиотеками, ORM, обработкой изображений или долгой серией jobs:
cron → новый worker → обработка jobs → достигнут лимит → worker завершён
→ следующий cron запускает чистый PHP-процесс
Но это не спасает, если одна job сразу превышает доступную память или работает дольше допустимого timeout. Такие jobs нужно проектировать порционно.
Плохо:
$users = User::query()->get();
foreach ($users as $user) {
// Обработка всех пользователей в одной job
}
Лучше:
User::query()
->select(['id', 'email'])
->lazyById(500)
->each(function (User $user): void {
// Обработка одного пользователя
});
Ещё надёжнее — создавать отдельную job для небольшой единицы работы: одного письма, одного пользователя, одного файла или одной страницы импорта.
7. Retry и необратимые действия
Для обычной job можно задать попытки и задержку между ними:
final class FetchRemoteDataJob implements ShouldQueue
{
public int $tries = 3;
public function backoff(): array
{
return [30, 120];
}
}
После исчерпания попыток Laravel перенесёт job в failed_jobs. Посмотреть список можно командой:
php artisan queue:failed
Для действий с внешним необратимым эффектом — например, отправки сообщения через сторонний API — не всегда безопасно бездумно включать повторы. Если внешний сервис принял запрос, а worker завершился до записи локального статуса, возможен дубль.
Рекомендация:
- хранить состояние операции в БД;
- делать job идемпотентной;
- использовать отдельную запись доставки на одного получателя;
- перед внешним вызовом атомарно захватывать запись;
- выбирать
tries=1, если повтор без идемпотентного ключа опаснее, чем ручная повторная отправка.
Сравнение с WordPress и wp-queue
Пакет wp-queue добавляет Laravel-подобную очередь поверх WordPress. В частности, его worker умеет ограничивать число jobs, время работы и потребление памяти, а при повторных попытках применяет exponential backoff.
У wp-queue worker берёт лимит памяти из WP_MAX_MEMORY_LIMIT, затем из WP_MEMORY_LIMIT (по умолчанию — 256 МБ). После завершения каждой job он измеряет потребление памяти и при достижении 85% лимита завершает worker после текущей job. Также он останавливается по maxJobs, maxTime или при полном превышении memory limit.
Laravel уже предоставляет эти базовые возможности встроенно — через queue:work, настройки job и database queue driver. Параметр --memory задаёт лимит для Laravel worker-а; в сочетании с --max-jobs и короткоживущим cron-процессом это выполняет ту же практическую задачу: не давать одному PHP-процессу бесконечно накапливать память.
Важно: ни wp-queue, ни Laravel worker не способны гарантированно предотвратить OOM внутри одной чрезмерно тяжёлой job — их проверки происходят между jobs. Поэтому лимиты worker-а дополняют, но не заменяют порционную обработку.
| Возможность | wp-queue |
Встроенный Laravel |
|---|---|---|
| Хранение очереди в БД | Да | Да, driver database и таблица jobs |
| Запуск worker-а | Собственный Worker |
php artisan queue:work |
| Лимит времени worker-а | setMaxTime() |
--max-time |
| Лимит jobs за запуск | setMaxJobs() |
--max-jobs |
| Контроль памяти | Остановка worker-а при высоком потреблении | --memory и перезапуск cron-ом |
| Повторные попытки | Exponential backoff | $tries, $backoff, backoff() |
| Failed jobs | Собственный лог/обработчики | failed_jobs, failed() |
| Цепочки и batch | Да | Bus::chain(), Bus::batch() |
| UI мониторинга | Встроен | Horizon требует Redis; без Redis — логи, БД и свои admin-страницы |
Проверка после настройки
Проверьте расписание:
php artisan schedule:list
Проверьте failed jobs:
php artisan queue:failed
Запустите worker вручную для диагностики:
php artisan queue:work database --queue=default --once -vvv
Проверьте, что:
- HTTP-запрос создаёт запись в
jobs; - cron worker забирает и удаляет успешную job;
- ошибка появляется в
failed_jobsпосле исчерпания попыток; - файлы логов cron доступны для записи;
- не возникают параллельные процессы благодаря
flock.
Итоговые рекомендации
Для небольшого и среднего Laravel-проекта на shared-хостинге:
- Используйте
QUEUE_CONNECTION=database, а неsync. - Запускайте
schedule:runcron-ом каждую минуту. - Запускайте отдельный короткий
queue:workcron-ом каждую минуту. - Всегда используйте
flock,--stop-when-emptyи--max-time, чтобы процессы не пересекались. - Добавляйте
--max-jobsи--memory, чтобы PHP-процесс регулярно перезапускался с чистой памятью. - Разделяйте критичные и тяжёлые jobs по именованным очередям.
- Проектируйте тяжёлые jobs маленькими и идемпотентными.
- Проверяйте
failed_jobsи cron-логи после каждого изменения.
Если проекту нужна обработка jobs почти без задержки, высокая параллельность или визуальный мониторинг очередей, следующим шагом будет VPS/Docker с постоянным worker-ом. Redis и Laravel Horizon стоит добавлять, когда их преимущества действительно нужны, а не только ради работы очередей.