Планировщик задач в Laravel: типичные ошибки и как их избежать
Задача помечена каждую минуту, а запускается раз в час. Или дублируется на двух серверах. Разбираем, почему scheduler в Laravel ведёт себя не так, как написано в коде, и как это чинить.

Вы прописали everyMinute(), задеплоили, а задача молчит. Или наоборот: отчёт, который должен уходить раз в сутки, приходит клиенту дважды. Планировщик Laravel — не отдельный демон, а обычный код, который дёргается один раз в минуту из cron. Большинство проблем растёт именно из непонимания этой связки. Разбираем ошибки, которые повторяются на каждом втором проекте, — на Laravel 10.x и 11.x.
Ошибка 1. Забыли про единственную строку в crontab
Самая частая причина «планировщик не работает» — его вообще никто не запускает. Всё расписание внутри приложения крутится поверх одной системной задачи, которая каждую минуту вызывает schedule:run. Нет этой строки — нет и планировщика, сколько бы методов вы ни навесили.
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
Строка добавляется в crontab пользователя, от которого работает PHP (обычно не root, а www-data или отдельный deploy-пользователь). Проверить, что задача зарегистрирована: crontab -l. Если строки нет — планировщик молчит независимо от кода.
Cron вызывает schedule:run раз в минуту. Всё, что мельче минуты, планировщик Laravel делать не умеет — это ограничение самого cron, а не фреймворка.
Где описывать расписание, зависит от версии. В Laravel 10.x — метод schedule() в app/Console/Kernel.php. В Laravel 11.x класса Kernel по умолчанию нет: расписание пишут в routes/console.php через фасад Schedule. Если тащите гайд из старой статьи в новый проект — сверьтесь с версией, иначе будете искать несуществующий файл.
Ошибка 2. Долгая задача накладывается сама на себя
Задача помечена everyMinute() и обрабатывает очередь писем. В обычный день она укладывается в 20 секунд. В день рассылки — в три минуты. Cron за это время запустит её ещё дважды, и три копии начнут расталкивать друг друга по одним и тем же записям. Итог — дубли писем и гонка за строки в базе.
Лечится методом withoutOverlapping(): пока предыдущий запуск не завершился, новый не стартует.
use Illuminate\Support\Facades\Schedule;
// routes/console.php (Laravel 11.x)
Schedule::command('emails:send-digest')
->everyMinute()
->withoutOverlapping();
Есть подводный камень. Блокировка держится через кеш*, и по умолчанию живёт 24 часа. Если процесс упал жёстко (kill -9, OOM), лок может остаться и заблокировать задачу на сутки. Задавайте разумный TTL блокировки в минутах:
Schedule::command('emails:send-digest')
->everyMinute()
->withoutOverlapping(10); // лок живёт максимум 10 минут
* Лок — распределённая блокировка. Laravel хранит её в драйвере кеша (Redis, файлы, база). Если драйвер — array, блокировка не переживёт даже один запуск, потому что живёт только в памяти процесса.
Ошибка 3. Драйвер кеша array или разные кеши на серверах
С withoutOverlapping() связана вторая ловушка. Если CACHE_STORE (в старых версиях — CACHE_DRIVER) стоит array, блокировка не работает вообще: каждый запуск schedule:run — отдельный процесс со своей пустой памятью. То же с onOneServer() — он тоже опирается на общий кеш.
На проде под планировщик нужен общий персистентный кеш, лучше Redis. Если приложение раскатано на несколько серверов и на каждом свой cron, а кеш локальный — onOneServer() не спасёт, потому что серверы не видят чужих блокировок. Нужен один общий Redis или база на все узлы.
Ошибка 4. Задача выполняется на каждом сервере кластера
Разложили приложение на три ноды, на каждой прописали cron. Итог — ежедневный отчёт формируется трижды, а списание с баланса уходит три раза. Метод onOneServer() гарантирует, что за конкретный запуск возьмётся ровно один сервер.
Schedule::command('reports:daily')
->dailyAt('03:00')
->onOneServer();
Два условия, без которых он молча не сработает: общий кеш на все ноды (см. ошибку 3) и уникальное имя задачи. Замыкания без явного имени onOneServer() не поддерживает — используйте команды или задайте имя через name().
Ошибка 5. Ошибка внутри задачи уходит в тишину
По умолчанию вывод schedule:run в crontab отправлен в /dev/null. Задача падает с исключением — вы об этом не узнаете, пока клиент не спросит, где отчёт. Планировщик не бросает ошибку наружу так, чтобы её было видно.
Что подключить:
- вывод команды в файл —
->appendOutputTo(storage_path('logs/reports.log')); - хук на провал —
->onFailure(fn () => ...)с уведомлением; - внешний пинг-мониторинг через
->pingOnSuccess()и->pingOnFailure()— сервис вроде heartbeat-мониторинга сообщит, если задача перестала отмечаться.
Schedule::command('reports:daily')
->dailyAt('03:00')
->onOneServer()
->appendOutputTo(storage_path('logs/reports.log'))
->onFailure(function () {
// уведомить дежурного
\Log::error('Ежедневный отчёт не сформирован');
});
Ошибка 6. Тяжёлая работа висит прямо в задаче планировщика
В расписании стоит команда, которая генерирует PDF по десяти тысячам заказов. Она блокирует schedule:run на минуты, и остальные задачи из того же прогона ждут её. Планировщик — это диспетчер, а не воркер. Долгую работу отправляйте в очередь.
Schedule::call(function () {
GenerateReports::dispatch(); // задача уходит в очередь
})->dailyAt('03:00');
Тогда schedule:run отрабатывает мгновенно, а тяжёлое считает воркер queue:work. Не путайте два процесса: планировщик решает когда, очередь — как долго.
Ошибка 7. Не учли часовой пояс
Время в расписании берётся из APP_TIMEZONE (в конфиге config/app.php). Если там UTC, а вы ждёте dailyAt('09:00') по Москве, отчёт уйдёт в полдень. Либо задайте пояс приложения, либо явно на задаче:
Schedule::command('reports:daily')
->dailyAt('09:00')
->timezone('Europe/Moscow');
Отдельно держите в голове переход на летнее время в поясах, где он есть: в такие даты задача, назначенная на пропавший час, может не выполниться или выполниться дважды. В России перевода часов нет, но если клиенты в ЕС — проверяйте.
Сводка методов защиты
| Метод | От чего защищает | Что нужно, чтобы работал |
|---|---|---|
withoutOverlapping(int $minutes) | наложение долгой задачи на саму себя | персистентный кеш, разумный TTL лока |
onOneServer() | дубли на нескольких серверах | общий кеш на все ноды, имя задачи |
appendOutputTo() / onFailure() | тихие падения | писать в лог или слать уведомление |
| вынос в очередь | блокировку прогона тяжёлой работой | запущенный queue:work |
timezone() | сдвиг времени запуска | верный пояс на задаче или в конфиге |
Как проверить расписание локально
- Посмотреть список зарегистрированных задач и ближайшее время запуска:
php artisan schedule:list(доступна с Laravel 9.x). - Прогнать планировщик вручную один раз:
php artisan schedule:run— увидите, что реально запустится в эту минуту. - Держать планировщик в фоне без реального cron:
php artisan schedule:work— команда сама дёргаетschedule:runраз в минуту, удобно на локальной машине.
Если задача не появляется в schedule:list — проблема в регистрации (не тот файл, не тот класс), а не в cron. Если появляется, но не отрабатывает на сервере — смотрите crontab и права пользователя.
Что настроить дальше
Три вещи, которые стоит проверить сразу после деплоя планировщика в прод:
- отдельный процесс
queue:workпод супервизором (Supervisor или systemd) — иначе задачи, отправленные в очередь, будут копиться и не выполняться; - внешний heartbeat-мониторинг на критичные задачи: молчание планировщика само себя не заметит;
- ротация логов, в которые пишет
appendOutputTo(), чтобы не забить диск.
Инвалидация и очистка старых логов — отдельная тема; здесь мы разобрали только поведение самого scheduler. Точный набор доступных методов и их сигнатуры сверяйте с документацией вашей версии Laravel — между 10.x и 11.x поменялось место регистрации расписания.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: Laravel Task Scheduling — официальная документация, Laravel Queues — официальная документация
Частые вопросы
У меня Laravel 11, а в примерах Kernel.php — что делать?
Прописал withoutOverlapping, но задача всё равно дублируется
Что будет, если я пропущу срок и задача не запустится?
Почему schedule:list показывает задачу, а на сервере она не работает?
Можно ли запускать задачу чаще, чем раз в минуту?
Нужен ли отдельный процесс для очередей, если задачи уходят через dispatch?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.