Планировщик задач в Laravel: как сделать правильно
Один cron-вызов раз в минуту вместо десятка записей в crontab. Разбираем, как настроить планировщик Laravel так, чтобы задачи не наслаивались, не падали молча и переживали деплой.

Планировщик Laravel сводит все периодические задачи к одной записи в crontab, которая дёргает php artisan schedule:run раз в минуту. Дальше расписание живёт в коде, под контролем версий, а не в разрозненных строках cron на сервере. Разберём подходы к тому, как описывать задачи, что делать с наложением запусков, как не потерять единственный вызов при переезде на новый сервер и почему очередь — не то же самое, что планировщик. Статья про Laravel 11.x и 12.x, где расписание задаётся в routes/console.php; для 10.x и раньше отметим отличия отдельно.
Задача и почему «просто добавить в crontab» ломается
Классический путь — прописать десять строк в crontab: одна на рассылку, одна на очистку логов, одна на пересчёт статистики. Проблема в том, что расписание оказывается вне репозитория. Новый разработчик не видит, что и когда запускается. При переезде на другой сервер половина задач теряется. Тестировать такое расписание нельзя, откатить одним коммитом — тоже.
Laravel предлагает обратное: одна строка в crontab, а всё расписание описано в коде приложения.
* * * * * cd /path-to-project && php artisan schedule:run >> /dev/null 2>&1
Этот вызов срабатывает каждую минуту, планировщик сам решает, какие задачи пора запускать. Но у такого подхода есть свои грабли, и почти все они всплывают не на локальной машине, а в проде под нагрузкой.
Где описывать задачи в Laravel 11 и 12
В Laravel 11 файл app/Console/Kernel.php убрали. Метод schedule() переехал в routes/console.php — там же, где регистрируются artisan-команды. Это ломает большинство старых гайдов: если вы читаете инструкцию 2023 года, она отправит вас в несуществующий класс.
<?php
use Illuminate\Support\Facades\Schedule;
// Вызов artisan-команды по расписанию
Schedule::command('emails:send')->dailyAt('03:00');
// Замыкание прямо в расписании
Schedule::call(function () {
// пересчёт агрегатов
})->hourly();
В Laravel 10.x и раньше то же самое живёт в app/Console/Kernel.php в методе schedule(Schedule $schedule), и вызов выглядит как $schedule->command(...). Логика одна, отличается только точка входа.
Четыре способа запустить работу — и цена каждого
Планировщик умеет запускать задачу минимум четырьмя способами. Выбор влияет на надёжность, на то, где вы увидите ошибку, и на то, блокирует ли задача сам процесс schedule:run.
1. Замыкание — Schedule::call()
Код выполняется в том же процессе, что и schedule:run. Быстро писать, удобно для мелочи. Плата: исключение внутри замыкания может уронить весь запуск планировщика, и остальные задачи этой минуты не выполнятся. Для тяжёлой или ненадёжной работы не годится.
2. Artisan-команда — Schedule::command()
Задача оформлена как отдельная команда, планировщик запускает её отдельным процессом через Artisan::call. Ошибка команды не валит остальное расписание. Это основной рабочий вариант для регулярных задач приложения.
3. Системная команда — Schedule::exec()
Запуск произвольного бинарника: бэкап базы через pg_dump, вызов внешнего скрипта. Планировщик не знает, что творится внутри, поэтому весь контроль за кодом возврата и логами на вас.
4. Задание в очередь — Schedule::job()
Планировщик только кладёт job в очередь, а выполняет её worker. Сам schedule:run отрабатывает за миллисекунды и не блокируется. Это правильный способ для всего, что длится дольше пары секунд.
| Способ | Отдельный процесс | Ошибка валит расписание | Когда брать |
|---|---|---|---|
call() | Нет | Да | Мелкая быстрая логика |
command() | Да | Нет | Регулярные задачи приложения |
exec() | Да | Нет | Внешние бинарники, бэкапы |
job() | Через worker | Нет | Долгая работа, интеграции |
Правило, которое экономит часы отладки: если задача может длиться дольше минуты, она не должна выполняться внутри schedule:run. Отправляйте её в очередь и дайте worker'у разбираться.
Наложение запусков — самая частая боль
Задача рассчитана на 20 секунд, но однажды база тормозит и она идёт три минуты. Планировщик за это время запустит её ещё дважды. Три копии одного пересчёта работают параллельно, портят данные и вешают сервер. Это не гипотетика, а типичный инцидент на любом проекте, где забыли про защиту от наложения.
Решение — withoutOverlapping(). Метод ставит блокировку, и пока предыдущий запуск не завершился, новый не стартует.
Schedule::command('stats:recalculate')
->everyMinute()
->withoutOverlapping();
Тонкий момент: блокировка хранится в кеше и по умолчанию держится 24 часа. Если процесс убили жёстко (OOM killer, kill -9), блокировка может остаться висеть, и задача перестанет запускаться. Задавайте разумный срок явно — например, withoutOverlapping(10) отпустит блокировку через 10 минут. Значение подбирайте по реальной длительности задачи.
Один сервер против нескольких
withoutOverlapping() защищает от наложения в пределах одного сервера. Если schedule:run крутится на нескольких машинах разом, задача выполнится на каждой. Для такого случая есть onOneServer() — но он требует общего кеша (Redis, Memcached, база), доступного всем серверам. С файловым или локальным кешем метод молча не сработает, и вы получите дубли в проде.
Schedule::command('reports:daily')
->daily()
->onOneServer();
Куда девается вывод и где искать ошибку
По умолчанию всё, что печатает задача, уходит в никуда. Когда что-то сломалось, вы об этом не узнаете. Прикрутите вывод и уведомления явно:
->sendOutputTo($path)— записать вывод в файл;->appendOutputTo($path)— дописать, а не перезаписать;->emailOutputOnFailure($address)— письмо, только если задача упала;->onFailure(fn () => ...)и->onSuccess(...)— колбэки по результату.
Отдельно проверьте: работает ли планировщик вообще. Есть soft-сигнал — если crontab-строка не добавлена или неверна, schedule:run никогда не вызовется, и вы будете чинить логику задачи, которая на самом деле просто не запускается.
Как отлаживать локально
Держать cron на машине разработчика неудобно. Начиная с Laravel 8, есть команда, которая имитирует cron: она сама вызывает schedule:run каждую минуту, пока запущена.
php artisan schedule:work
Посмотреть, что и когда сработает, без ожидания помогает php artisan schedule:list — она выводит все зарегистрированные задачи с их расписанием и следующим временем запуска. Если задачи нет в этом списке, до cron дело даже не дойдёт.
Если не сработало
- Задачи вообще не запускаются. Проверьте crontab:
crontab -l. Строка сschedule:runдолжна быть, путь к проекту — абсолютный, php — тот же, что в проде. Частая причина: cron использует другой PHP или другого пользователя без прав на storage. - Class not found на Kernel.php. Вы на Laravel 11+ и пишете задачи в
app/Console/Kernel.php, которого больше нет. Перенесите вroutes/console.php. - Задача выполняется несколько раз. Либо нет
withoutOverlapping(), либо несколько серверов безonOneServer()и общего кеша. - onOneServer не помогает. Драйвер кеша локальный (file, array). Переключите на Redis или базу, доступную всем машинам.
- Задача перестала запускаться после падения. Зависла блокировка
withoutOverlapping. Очистите кеш блокировки или задайте меньший таймаут аргументом метода.
Что настроить дальше
Планировщик — это триггер, а не исполнитель тяжёлой работы. Следующий разумный шаг: вынести долгие задачи в очередь через Schedule::job() и поднять worker под supervisor или php artisan queue:work с автоперезапуском. Отдельно настройте мониторинг — внешний heartbeat-сервис, который ждёт пинга от вашей ежеминутной задачи и алертит, если пинг пропал: так вы узнаете о молчащем cron раньше, чем клиенты. Инвалидация и повторные попытки в очередях — отдельная тема, здесь мы разобрали только сам планировщик.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: Task Scheduling — Laravel Documentation
Частые вопросы
А если у меня Laravel 10 или 9, код будет работать?
Нужно ли добавлять отдельную строку в crontab на каждую задачу?
Что будет, если задача не успевает выполниться за минуту?
Как проверить расписание, не дожидаясь срабатывания cron?
Почему onOneServer запускает задачу на всех серверах, а не на одном?
Задача упала в проде, но я об этом не узнал. Что настроить?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.