Cron job это задача, которая сама запускается в заданное время или через равные интервалы: каждую ночь в 02:00 делать бэкап, каждое утро отправлять отчёт о продажах, каждый час обновлять статусы доставки, первого числа месяца выставлять счета по подпискам. Название пришло от программы cron в Unix, где расписание задаётся выражением из пяти полей «минута час день месяц день-недели». Запланировать задачу легко; сложно сделать так, чтобы она не запускалась дважды, чтобы пропущенный запуск был замечен и чтобы она не ломалась молча. В статье разбираем cron-выражение, самые частые причины сбоев и безопасную схему, которую мы применяем в проектах заказной разработки.
Как читать cron-выражение?
Слева направо пять полей: минута (0-59), час (0-23), день месяца (1-31), месяц (1-12) и день недели (0-6, воскресенье 0). Звёздочка значит «каждый», запятая задаёт список, дефис диапазон, косая черта шаг. Несколько примеров:
- 0 2 * * *: каждый день в 02:00 (ночной бэкап, дневной отчёт).
- */15 * * * *: каждые 15 минут (синхронизация статусов доставки или оплаты).
- 0 9 * * 1-5: по будням в 09:00 (утренняя сводка).
- 0 0 1 * *: в полночь первого числа месяца (ежемесячные счета, продление подписок).
- 30 23 * * 0: каждое воскресенье в 23:30 (еженедельная очистка, архивирование старых записей).
Cron-выражение интерпретируется в каком-то часовом поясе, и обычно это часы сервера. Если сервер работает в UTC, «0 2 * * *» означает 05:00 по Москве. Поэтому часовой пояс планировщика задаётся явно и не оставляется по умолчанию; в странах с переходом на летнее время задачи между 02:00 и 03:00 могут в один день года не запуститься вовсе или запуститься дважды.
Где запускается задача по расписанию?
- Crontab сервера: классическая схема с одним сервером. Просто, но при смене сервера задачу забывают, и она не версионируется вместе с кодом.
- Планировщик внутри приложения: библиотеки вроде node-cron, Quartz или Hangfire. Живёт вместе с кодом, но если запущено несколько экземпляров приложения, задача срабатывает в каждом.
- Планировщик платформы: Kubernetes CronJob, триггеры по расписанию у облачных провайдеров, Vercel Cron. Платформа запускает один раз, а работа остаётся в вашем коде.
- Повторяющиеся задачи в очереди: очереди вроде BullMQ или Sidekiq сами хранят описание задачи и сразу дают повторы и историю запусков.
Самая частая ошибка: задача запускается дважды
Когда приложение масштабируется до двух серверов, встроенный планировщик тоже удваивается, и ночной отчёт уходит дважды. Для отчёта это досадно, для счетов или платёжных поручений это серьёзная ошибка. Мало того, чтобы задача запускалась один раз; сама работа должна давать тот же результат при повторном выполнении. Надёжнее всего дать каждому запуску ключ периода, например «ежемесячные счета, 2026-10». При старте задача пытается создать запись с этим ключом; если запись уже есть, работу взял другой экземпляр, и этот завершается. Та же таблица хранит, когда задача началась, когда закончилась и была ли ошибка. Это та же идея, что и идентификатор события у получателей вебхуков, о которой мы писали в статье о вебхуках.
// 1-го числа каждого месяца в 03:00 по времени Турции: '0 3 1 * *' (tz: Europe/Istanbul)
export async function monthlyInvoicesJob(now = new Date()) {
const period = formatInTimeZone(now, 'Europe/Istanbul', 'yyyy-MM');
const runKey = 'monthly-invoices:' + period;
// Уникальный индекс на job_runs.run_key: второй экземпляр сюда не попадёт.
const run = await db.jobRuns.insertIfAbsent({
runKey,
startedAt: new Date(),
status: 'running',
});
if (!run) return; // этот период уже обработан или обрабатывается
try {
const subs = await db.subscriptions.dueForBilling(period);
for (const sub of subs) {
// Счёт уникален по периоду + подписке: прерванный запуск безопасно стартует заново.
await billing.createInvoiceOnce(sub.id, period);
}
await db.jobRuns.finish(runKey, 'succeeded');
} catch (err) {
await db.jobRuns.finish(runKey, 'failed');
throw err; // пусть дойдёт до системы отслеживания ошибок
}
}Самое опасное состояние задачи по расписанию не ошибка, а отсутствие запуска: ошибку записывают в лог и видят, а задача, которая не запустилась, молчит, пока кто-нибудь не заметит. Каждой критичной задаче нужен мониторинг, отвечающий на вопрос «когда был последний успешный запуск».
Пропущенные запуски и мониторинг
Если сервер перезагрузится ровно в момент запуска или планировщик будет выключен во время выкладки новой версии, запуск будет пропущен. Классический cron пропущенные запуски не догоняет. Поэтому критичные задачи защищают двумя способами. Во-первых, задача обрабатывает не только «сейчас», а все необработанные периоды: задача ежемесячных счетов выставляет их за каждый период без счетов, а не только за текущий месяц. Во-вторых, её мониторят снаружи: каждый успешный запуск отправляет сигнал на адрес мониторинга, и если сигнал не пришёл в ожидаемое время, команда получает тревогу. Куда отправлять ошибки и сигналы, мы описали в статье об отслеживании ошибок и логировании; для задач резервного копирования такой мониторинг обязателен, подробности в статье о резервном копировании данных.
Долгие задачи и очереди
Cron это триггер, а не исполнитель. Выставлять счета десяти тысячам подписчиков в одном цикле значит при ошибке на середине не знать, какие счета уже созданы, и рисковать пересечением со следующим запуском. Правильная схема: cron только кладёт работу в очередь, а воркеры очереди обрабатывают каждый элемент отдельно, с повторами. Тогда ошибка у одного подписчика не останавливает остальных, а нагрузка распределяется по дню. То же правило работает для задач, зависящих от внешнего сервиса, например электронных счетов: когда другая сторона тормозит, очередь ждёт, а задача не падает по таймауту; об этом в статье об интеграции электронных счетов. Где жить задачам, в отдельном сервисе или в том же приложении, решает архитектура; см. микросервисы или монолит.
Чек-лист перед запуском
- Часовой пояс задан явно, а часы перехода на летнее время не используются?
- При нескольких экземплярах приложения задача всё равно срабатывает один раз?
- Повторное выполнение работы даёт тот же результат (ключ периода, уникальный индекс)?
- Пропущенный период догоняется при следующем запуске?
- Последний успешный запуск отслеживается и при просрочке вызывает тревогу?
- Описание задачи версионируется вместе с кодом и проверяется в CI/CD-конвейере?
Итог
Cron job это небольшая часть системы, которая вместо человека делает работу, о которой нельзя забыть; но небольшой размер не повод оставлять её без контроля. Безопасная задача по расписанию задаётся с явным часовым поясом, работает в одном экземпляре, не вредит при повторном запуске, догоняет пропущенные периоды и сообщает, когда не запустилась. Если вы хотите построить задачи выставления счетов, отчётов, синхронизации и бэкапов по этим принципам, посмотрите наше направление заказной разработки или сразу запросите смету.