Парсер выражений cron

Прочитайте выражение cron обычным языком и узнайте, когда именно оно сработает в следующий раз.

минута · час · день месяца · месяц · день недели. Макросы вроде @daily тоже работают.

Примеры:

Запускается

в 03:00, по понедельникам

Ближайшие шесть запусков · UTC

  1. 2026-09-21 03:00понедельник
  2. 2026-09-28 03:00понедельник
  3. 2026-10-05 03:00понедельник
  4. 2026-10-12 03:00понедельник
  5. 2026-10-19 03:00понедельник
  6. 2026-10-26 03:00понедельник

Показано в UTC. Настоящий crontab работает в часовом поясе сервера — именно там переход на летнее время и создаёт проблемы, см. ниже.

Поле за полем

минута
00
час
33
день месяца
*любое
месяц
*любое
день недели
11

Что стоит знать об этом расписании

  • 31-е число есть не в каждом месяце, поэтому в таких месяцах задание просто не запустится.
  • 29 февраля бывает только в високосные годы — значит, запуск раз в четыре года.

Пять полей, самое грубое — последнее

ПозицияПолеДиапазонПримечания
1Минута0–59
2Час0–2324-часовой формат, время сервера
3День месяца1–31Несуществующие дни просто пропускаются
4Месяц1–12Или JANDEC
5День недели0–7И 0, и 7 означают воскресенье. Или SUNSAT

Внутри поля: * — любое значение, , перечисляет, - задаёт диапазон, / — шаг. */15 в поле минут — это каждые пятнадцать минут; 5/10 означает «от 5 и далее через десять», то есть 5, 15, 25 и так далее. Диапазоны могут заворачиваться, поэтому FRI-MON — корректный способ сказать «выходные и их края».

Правило, на котором спотыкаются все

Вот единственное, что стоит унести с этой страницы.

Когда ограничены оба поля — день месяца и день недели, — cron запускает задачу, если совпадает любое из них. Не оба. Это «или», а читается как «и».

0 0 1 * MON   # NOT "the 1st, if it is a Monday"
              # Actually: every 1st, AND every Monday

Такое расписание срабатывает примерно пять раз в месяц. Вставьте его в разборщик выше и посмотрите на ближайшие запуски — последовательность выдаёт это сразу.

Правило действует только тогда, когда ограничены оба поля. Если хотя бы одно равно *, всё определяет другое, поэтому 0 0 * * MON ведёт себя ровно так, как ожидается. Поведение прописано в спецификации POSIX, и его реализует любой распространённый cron, так что это не ошибка, которую можно отключить настройкой.

Чтобы действительно требовать оба условия, ограничьте одно поле в расписании, а второе проверьте в начале самой задачи:

0 0 * * MON  [ "$(date +\%d)" = "01" ] && /usr/local/bin/job

Переход на летнее время, который ломает задачи дважды в год

Если не сказано иное, cron работает в местном часовом поясе сервера, а местное время не непрерывно. При переводе часов вперёд час исчезает: задача на 02:30 может быть пропущена целиком или запущена в 03:30 — смотря какой у вас cron. При переводе назад 01:30 наступает дважды, и задача может отработать столько же.

Современный Vixie cron старается — пропущенные из-за скачка вперёд задачи обычно выполняются один раз позже, — но поведение неодинаково в разных реализациях, а планировщики контейнеров добавляют ещё разнобоя.

Долговечное решение — держать планировщик в UTC и преобразовывать на границах. CronJob в Kubernetes по умолчанию работает в UTC именно поэтому. Если задача обязана выполняться в конкретное местное время, смиритесь, что часть года она будет уходить на час, — или возьмите планировщик, который по-настоящему понимает часовые пояса: таймеры systemd понимают.

Другие причины, по которым задача cron не выполняется

  • Окружение почти пусто. Cron не подгружает профиль вашей оболочки, поэтому PATH минимален, а виртуальное окружение не активировано. Всегда используйте абсолютные пути.
  • Голый знак процента. В crontab % означает перевод строки. date +%Y тихо ломается; писать надо date +\%Y.
  • Нет перевода строки в конце. Некоторые cron игнорируют последнюю строку crontab, если она не оканчивается им.
  • Вывод уходит в никуда. Cron отправляет вывод письмом пользователю, и если почта не настроена, он пропадает. Перенаправьте в файл журнала, чтобы отказы были видны.
  • Наложение запусков. Cron не ждёт завершения предыдущего запуска. Пятиминутная задача в ежеминутном расписании накапливается, пока что-нибудь не рухнет. Используйте flock.

Чего cron выразить не может

Поля независимы, поэтому всё, что не делится нацело на час или сутки, недостижимо. «Каждые 90 минут» записать нельзя. Как и «последнюю пятницу месяца» в стандартном cron — для этого и существуют L и # в Quartz, и потому же они непереносимы.

Если вы ловите себя на борьбе с синтаксисом, это обычно сигнал перейти к планировщику, который мыслит интервалами, а не календарными полями. Таймер systemd с OnUnitActiveSec=90min говорит то, что вы имели в виду, одной строкой.

Смежное

Времена запусков выше указаны в UTC — конвертер меток времени переведёт любое из них в ваш часовой пояс или в тот формат, которым пишет журналы ваш планировщик. Если вам нужен поиск по тексту, а не расписание, тестер регулярных выражений — по соседству.

Вопросы о cron

Почему моя задача запускается в дни, которые я не назначал?

Почти наверняка из-за правила «день месяца и день недели». Когда оба поля ограничены, cron запускает задачу, если совпадает ЛЮБОЕ из них, а не оба сразу. Поэтому 0 0 1 * MON срабатывает и первого числа каждого месяца, и каждый понедельник — то есть примерно пять раз в месяц вместо одного задуманного. Если хотя бы одно поле равно *, правило не применяется и всё определяет второе. Чтобы требовать оба условия, ограничьте одно поле в cron, а второе проверяйте внутри самой задачи.

Что означают пять полей?

Минута (0–59), час (0–23), день месяца (1–31), месяц (1–12), день недели (0–7, где и 0, и 7 — воскресенье). Слева направо шаг становится всё крупнее — полезный мнемонический приём. Некоторые планировщики — Quartz, Spring и ряд исполнителей задач — добавляют впереди шестое поле для секунд; в стандартном crontab поля секунд нет вовсе.

Что происходит с задачей cron при переходе на летнее время?

Зависит от реализации, и именно здесь запланированные задачи тихо ломаются дважды в год. При переводе часов вперёд одного часа просто не существует: задача на 02:30 может быть пропущена целиком или выполниться в 03:30 — как решит конкретный cron. При переводе назад 01:30 наступает дважды, и задача может отработать дважды. Современные Vixie cron и таймеры systemd стараются вести себя разумно, старые — нет. Надёжный ответ: выполнять запланированную работу в UTC, а преобразовывать только на границах.

Что такое L, W и # и почему они здесь отвергаются?

Это расширения Quartz: L — «последний» (последний день месяца, последняя пятница), W — ближайший рабочий день, # — «n-й такой-то день недели в месяце». Они по-настоящему удобны и при этом не являются стандартным cron. Ни crontab, ни CronJob в Kubernetes, ни GitHub Actions их не примут или поймут неправильно. Этот разборщик отказывает, а не гадает, потому что расписание, молча проглотившее L, ошибётся ровно тем способом, который никто не проверяет.

Что делает @reboot?

Запускает задачу один раз при старте cron, обычно при загрузке системы. Периодического расписания у неё нет, поэтому и вычислять нечего — потому инструмент так и пишет, вместо того чтобы показывать пустой список. Полезно знать, что @reboot срабатывает при запуске демона cron, а не когда машина закончила загружаться, так что всё, что зависит от поднявшейся сети, должно ждать её самостоятельно.

Как запускать что-то каждые 90 минут?

Напрямую это не выражается, и это настоящее ограничение, а не приём, которого вы не выучили. Поля cron независимы, поэтому */90 в поле минут недопустимо, и составить расписание, не делящее нацело час или сутки, невозможно. Обходные пути — две записи, покрывающие нужный рисунок в пределах суток, или, если честнее, планировщик, мыслящий интервалами, а не календарными полями: например, таймер systemd с OnUnitActiveSec.

Последняя проверка . Заметили, что что-то устарело? Напишите нам.