Планировщик задач Laravel я подключаю почти в каждом бэкенд-проекте, где нужна рассылка отчётов по ночам, синхронизация остатков или чистка временных файлов. Работает связка из двух частей: сам Laravel с фасадом Schedule, который знает, что и когда запускать, и один cron на сервере, который раз в минуту дёргает artisan schedule:run. Ниже - как это настроить с нуля и на что я натыкался на реальных проектах.
Как устроен планировщик задач в Laravel
До Laravel 11 расписание задач жило в методе schedule() класса app/Console/Kernel.php. С одиннадцатой версии Kernel-классы убрали, и задачи регистрируются прямо в routes/console.php через фасад Schedule. Логика та же самая, поменялось только место, где я это пишу:
// routes/console.php, Laravel 11+
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:generate')->dailyAt('01:00');
Schedule::call(function () {
// например, синхронизация остатков с сайтом
})->everyFiveMinutes()->withoutOverlapping();
Для проектов на более ранних версиях тот же код пишется в Kernel:
// app/Console/Kernel.php, Laravel до 11 версии
protected function schedule(Schedule $schedule)
{
$schedule->command('reports:generate')->dailyAt('01:00');
}
При апгрейде старого проекта на Laravel 11 переписывать ничего не обязательно - Kernel-подход продолжает работать, просто новые задачи я уже добавляю в routes/console.php, чтобы не плодить два места с расписанием. Смысл планировщика в том, что вместо десятка строк в crontab с разными командами и временем я держу одну точку входа - artisan schedule:run. Она запускается каждую минуту и сама решает, какие задачи из списка пора выполнять прямо сейчас, а какие ещё рано. Это удобно, потому что расписание версионируется в git вместе с кодом, а не живёт отдельно на сервере, где его легко забыть или потерять при переезде на новый хостинг.
Если под задачей понимается не разовый скрипт, а часть архитектуры проекта, скажем, ночная выгрузка заказов во внешнюю систему или пересчёт остатков по расписанию, такие вещи я закладываю сразу на этапе разработки бэкенда на Laravel, а не добавляю потом поверх готового кода.
Настройка cron для запуска Laravel-планировщика на сервере
На сервере нужна ровно одна строка в crontab. Открываю редактор для пользователя, от имени которого крутится сайт (обычно www-data или deploy, не root):
crontab -e
И добавляю строку с путём до проекта:
* * * * * cd /var/www/example.com && php artisan schedule:run >> /dev/null 2>&1
Cron физически не умеет запускать что-то чаще раза в минуту, поэтому строка всегда одна и та же вне зависимости от того, сколько задач реально стоит в расписании внутри Laravel: хоть одна, хоть пятьдесят. Три момента, на которые смотрю в первую очередь при настройке:
- Путь до php должен указывать на нужную версию, если на сервере их несколько - лучше прописать полный путь вроде /usr/bin/php8.3, а не полагаться на php по умолчанию в PATH.
- Команда идёт из директории проекта (cd /var/www/example.com &&), иначе artisan не найдёт .env и конфиги.
- Проверяю, что crontab настроен именно у того пользователя, который реально владеет процессом веб-сервера, а не задвоен ещё и у root.
После добавления строки я запускаю php artisan schedule:list, чтобы увидеть таблицу зарегистрированных задач и время их следующего запуска - так сразу видно, подхватил ли Laravel список правильно.
Методы расписания: интервалы, cron-выражения и часовые пояса
У каждой задачи в цепочке методов задаётся периодичность. Использую как готовые хелперы, так и сырые cron-выражения, когда стандартных вариантов не хватает:
Schedule::command('backup:run')->daily();
Schedule::command('reports:weekly')->weeklyOn(1, '08:00');
Schedule::command('cleanup:temp')->hourly();
Schedule::command('stock:sync')->everyFiveMinutes();
Schedule::command('digest:send')->cron('0 9 * * 1-5')->timezone('Europe/Moscow');
Часовой пояс отдельная тема: сервер часто живёт в UTC, а бизнес ждёт письмо в девять утра по Москве. Не полагаюсь на глобальную настройку timezone в config/app.php - для конкретных задач ставлю метод timezone() явно, иначе после переезда на другой сервер расписание внезапно съезжает на несколько часов.
Бесплатный материал
🎁 Полезный скрипт в подарок
Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.
Без спама. Отписка в 1 клик.
withoutOverlapping и onOneServer: защита от дублей
Две вещи, без которых планировщик рано или поздно устраивает сюрприз в проде.
withoutOverlapping
Если задача выполняется дольше минуты, а cron всё равно дёргает schedule:run каждую минуту, есть риск запустить вторую копию той же задачи поверх первой. Метод withoutOverlapping() ставит блокировку через атомарный лок в кеше, и вторая попытка просто пропускается, пока первая не закончится. Для этого нужен cache-драйвер с поддержкой атомарных блокировок, на практике беру redis или database - файловый кеш для такой задачи не подходит, если приложение крутится на нескольких воркерах или в контейнерах.
Schedule::command('stock:sync')
->everyFiveMinutes()
->withoutOverlapping(10);
Число в скобках - таймаут блокировки в минутах на случай, если задача зависла и не сняла лок сама.
onOneServer
Когда приложение развёрнуто на нескольких серверах за балансировщиком, каждый из них по отдельности гоняет свой cron, и без дополнительной настройки одна и та же задача выполнится параллельно на всех машинах. Метод onOneServer() решает это через тот же механизм блокировок в общем кеше - задача выполняется только на той машине, которая первой захватила лок:
Schedule::command('reports:generate')
->dailyAt('01:00')
->onOneServer();
Работает это только если redis или другой центральный кеш общий для всех серверов, а не локальный на каждом из них.
Логи, уведомления и проверка расписания
Голый вывод artisan schedule:run в /dev/null удобен, пока всё работает, и бесполезен, когда что-то ломается посреди ночи. Для важных задач добавляю логирование и колбэк на ошибку:
Schedule::command('reports:generate')
->dailyAt('01:00')
->appendOutputTo(storage_path('logs/schedule.log'))
->onFailure(function () {
Log::error('Не удалось сформировать ночной отчёт');
});
В onFailure можно повесить что угодно - отправку в Slack, письмо, вызов вебхука для telegram-бота, который у меня и так часто крутится в проекте для уведомлений команды. Для визуального контроля добавляю Laravel Pulse - там видно и долгие job’ы из очередей, и зависшие команды, без того чтобы ковыряться в логах руками.
Отдельно держу в голове разницу между schedule:run и schedule:test: первый вызывает cron раз в минуту и выполняет всё, что подошло по времени, второй - интерактивная команда, которой я вручную прогоняю конкретную задачу из списка, не дожидаясь её реального времени, когда нужно быстро проверить, что она вообще отрабатывает без ошибок.
И ещё один момент, который путают чаще всего: планировщик и очереди решают разные задачи. Schedule по часам и дням запускает команды и колбэки, а queue:work - это долгоживущий процесс, обрабатывающий джобы, которые в очередь кладёт код приложения. Cron планировщика можно смело гонять каждую минуту, а вот queue:work через cron не запускают - для него нужен supervisor или systemd, которые перезапустят процесс, если он упадёт.
Типичные ошибки при настройке планировщика на проде
Большая часть проблем с планировщиком всплывает не на локали, а через пару недель на проде, когда накапливаются логи или подключается второй сервер:
| Ошибка | Чем оборачивается |
|---|---|
| Строка cron прописана дважды - вручную и через деплой-скрипт | Задачи выполняются по два раза, дублируются письма и записи в базе |
| Не указан полный путь до нужной версии php | Cron берёт системный php по умолчанию, который может отличаться от версии проекта |
| Тяжёлая логика написана прямо в замыкании Schedule::call | Задача блокирует следующий тик schedule:run, если не стоит withoutOverlapping |
| onOneServer включён без общего кеша между серверами | Задача либо не запускается нигде, либо всё равно дублируется |
По последнему пункту таблицы: если тяжёлая работа выполняется дольше пары секунд, обычно выношу её не в закрытие внутри schedule, а в отдельный джоб, который планировщик просто диспатчит в очередь, а обрабатывает уже queue:work под supervisor. Так cron остаётся лёгким и предсказуемым, а вся тяжёлая логика уходит туда, где для неё есть ретраи и нормальный контроль зависаний.
Laravel регулярно обновляется, поэтому перед внедрением я сверяюсь с актуальной документацией.
Частые вопросы
Как часто нужно запускать schedule:run через cron?
Раз в минуту, без исключений. Сам Laravel внутри разберётся, какие задачи из расписания реально пора выполнять на этой минуте, а какие ещё ждут своего часа - дополнительных cron-строк под каждую задачу заводить не нужно.
Нужен ли отдельный cron-job на каждую задачу в проекте?
Нет, и это одно из главных удобств планировщика. Вся логика расписания живёт в коде Laravel, на сервере остаётся одна строка cron, которая просто дёргает schedule:run, а список задач и их периодичность меняются через git, а не через ручное редактирование crontab.
Как проверить, что планировщик реально работает на сервере?
Запускаю php artisan schedule:list, чтобы увидеть таблицу задач и время следующего запуска, а для точечной проверки конкретной команды использую schedule:test, который выполняет выбранную задачу сразу, не дожидаясь её расписания.
Чем планировщик задач отличается от очередей в Laravel?
Schedule решает, когда что-то должно произойти - раз в час, раз в сутки, по cron-выражению. Очереди решают, как это выполнить асинхронно и с ретраями, если задача упала. На практике планировщик часто просто ставит джоб в очередь по расписанию, а сама тяжёлая работа выполняется уже воркером queue:work.