На большинстве серверов по-прежнему живёт старый добрый cron. Но во всех современных дистрибутивах Linux с systemd у нас есть альтернатива — systemd timers. Часто они уже используются самим дистрибутивом (обслуживание, обновления, ротация логов), но для своих задач админы продолжают привычно держаться за crontab.
В этой статье разберёмся, чем отличаются cron и systemd timers, когда логичнее использовать каждый из них, как правильно мигрировать периодические задачи и добавить нормальный healthcheck и логирование, вместо надежды на «наверное, оно работает».
Кратко: cron vs systemd timers
Сначала структурируем различия по сути, а не по идеологии.
Cron:
- Простой текстовый формат расписания (5 полей + команда).
- Минимум зависимостей, работает даже без
systemd. - Запуск без контекста сервисов: каждая строка — отдельный шелл.
- Логика перезапуска, лимиты ресурсов и безопасность — на совести скрипта.
Systemd timers:
- Каждый таймер привязан к
systemd-сервису. - Гибкий синтаксис расписания (
OnCalendar, монотонные таймеры). - Встроенный учёт пропущенных запусков,
RandomizedDelaySec, зависимостей. - Логи — в journald, единый механизм рестартов, лимитов ресурсов и sandboxing.
Ключевая идея: systemd timers — это не просто «ещё один cron», а расписание для
systemd-сервисов, со всеми их возможностями — от рестартов и лимитов ресурсов до зависимостей и изоляции.
Когда cron по‑прежнему уместен
Несмотря на популярность systemd, полностью отказываться от cron не всегда нужно.
Типичные случаи, когда cron всё ещё ок:
- Простые задания на shared-хостинге, где у пользователя нет доступа к
systemd(типичный случай для классического виртуального хостинга). - Легаси-окружения без
systemd(старые Debian/CentOS, контейнеры, минимальные системы). - Задачи, напрямую интегрированные с самим
cron(например, пакеты дистрибутива, ещё не мигрировавшие на systemd timers).
Если у вас небольшой скрипт, который раз в час синхронизирует файлы или крутит «хвосты» в базе, и сервер без systemd, — нет смысла усложнять.

Когда лучше перейти на systemd timers
Во всех более-менее серьёзных сценариях, особенно на VDS или выкупленных серверах с systemd, переход на таймеры оправдан.
Сигналы, что пора на systemd timers:
- Вы хотите видеть статус задач: когда действительно запускались, что с ними, почему упали.
- Нужно нормальное логирование в journald, интеграция с мониторингом, алёртами.
- Задача должна перезапускаться при сбое или иметь строгие лимиты по RAM/CPU.
- Нужна изоляция (sandbox), чёткие права,
User/Group, ограничения по файловой системе. - Нужен детальный
healthcheck: статус последнего прогона, код возврата, время выполнения.
На практике, как только задача становится хоть немного «продакшеновой» (бэкапы, интеграции с внешними API, обновление кэшей и т.п.), таймеры дают много плюсов почти бесплатно. Особенно это заметно на выделенных серверах и VDS для продакшен-проектов, где от стабильности периодических задач сильно зависит бизнес.
Базовая архитектура systemd timers
В systemd таймер — это всего лишь юнит с типом .timer, который привязан к юниту .service. Принцип:
- Конкретную работу описываем в
foo.service. - Расписание, когда её запускать, — в
foo.timer.
Простейший пример юнита сервиса:
[Unit]
Description=Example cron-like job using systemd
[Service]
Type=oneshot
User=www-data
Group=www-data
ExecStart=/usr/local/bin/my-job.sh
И соответствующий таймер:
[Unit]
Description=Run my-job every 5 minutes
[Timer]
Persistent=true
[Install]
WantedBy=timers.target
Далее:
systemctl daemon-reload
systemctl enable --now my-job.timer
Теперь systemd сам будет запускать my-job.service по расписанию и хранить информацию о предыдущих запусках.
Перенос простого crontab на systemd timers
Рассмотрим типичный пример из crontab -e:
*/10 * * * * /usr/local/bin/backup-db.sh > /var/log/backup-db.log 2>&1
Здесь каждые 10 минут запускается скрипт и пишет лог в файл. Чтобы перенести его в systemd, создадим два юнита: сервис и таймер.
Сервисный юнит
# /etc/systemd/system/backup-db.service
[Unit]
Description=Database backup job
[Service]
Type=oneshot
User=backup
Group=backup
ExecStart=/usr/local/bin/backup-db.sh
StandardOutput=journal
StandardError=inherit
Здесь мы убрали перенаправление в файл и используем journald. Так проще парсить логи мониторингом или собирать их в централизованное хранилище. Если вы уже строите резервные копии баз (например, по сценариям вроде описанных в руководстве по PITR для PostgreSQL), ведение логов через journald сильно упрощает отладку.
Таймер
# /etc/systemd/system/backup-db.timer
[Unit]
Description=Run database backup every 10 minutes
[Timer]
Persistent=true
RandomizedDelaySec=60
[Install]
WantedBy=timers.target
Опции:
OnCalendar=*:0/10— каждые 10 минут.Persistent=true— если сервер был выключен или в спящем режиме в момент запуска, пропущенный запуск будет выполнен при старте.RandomizedDelaySec=60— небольшой рандомный джиттер, чтобы не долбить, например, внешнее хранилище строго в одном и том же моменте (актуально при нескольких серверах).
После создания юнитов:
systemctl daemon-reload
systemctl enable --now backup-db.timer
Типичные «подводные камни» cron
Почему вообще стоит думать о миграции? В бою всплывают старые проблемы cron:
- Тихие падения: скрипт упал, о том, что задание не выполняется, узнаём через неделю от клиента.
- Переменные окружения: в
cronокружение минимальное, не подтягиваются пользовательские профили и алиасы, ломаютсяPATH,LANG,PYTHONPATH. - Параллельные запуски: если задача не успела завершиться до следующего запуска, копии накладываются, начинают конкурировать за ресурсы или данные.
- Нет чёткого статуса: чтобы понять, когда задание выполнялось, надо вручную смотреть системные логи или почту.
Часть проблем можно лечить в самом cron (например, через flock, логирование в syslog, ручной healthcheck), но systemd timers решают многие вопросы структурно.
Как systemd timers упрощают жизнь
Статус и логи по умолчанию
Для любого таймера и сервиса есть понятный статус:
systemctl status backup-db.timer
systemctl status backup-db.service
Логи:
journalctl -u backup-db.service
journalctl -u backup-db.service --since "2025-01-01" --until "2025-01-02"
Это автоматически даёт основу для healthcheck: можно мониторить коды возврата и отсутствие запусков.
Лимиты ресурсов и безопасность
В systemd для сервиса можно задать ограничения, чего в классическом cron нет:
[Service]
User=backup
Group=backup
MemoryMax=512M
CPUQuota=50%
RuntimeMaxSec=1800
ProtectSystem=strict
PrivateTmp=true
Таким образом, периодическое задание не «унесёт» весь сервер, если что-то пошло не так.
Работа с зависимостями
Можно указать, что задача должна запускаться только после того, как поднялась база, файловое хранилище и т.д.:
[Unit]
Description=Nightly report generator
Requires=mysqld.service
After=network-online.target mysqld.service
Для cron чего-то подобного приходится городить руками через скрипты и проверки.
Healthcheck периодических задач
Одна из главных причин, по которой имеет смысл уходить от «голого» cron, — нормальный healthcheck периодических задач. В идеале мониторинг должен уметь ответить на вопросы:
- Когда задание последний раз успешно выполнялось?
- Сколько оно выполнялось по времени?
- Каков код возврата?
- Не было ли таймаута или убийства по лимитам?
Базовый healthcheck через systemd
Из коробки можно использовать:
systemctl show -p ActiveState,SubState,ExecMainStatus,ExecMainExitTimestamp backup-db.service
А также парсить:
systemd-analyze calendar "*:0/10"
systemctl list-timers --all | grep backup-db
Часто этого достаточно, чтобы на стороне мониторинга (Prometheus-экспортёр, агент, свои скрипты) периодически спрашивать статус юнита и алертить, если, например, за N минут не было успешного выполнения.
Healthcheck на уровне скрипта
Лучше всего, когда сам скрипт возвращает адекватный exit code и пишет структурированные логи. Простейший пример оболочечного скрипта с учётом healthcheck:
#!/bin/bash
set -euo pipefail
log() {
echo "$(date -Is) [$$] $*"
}
log "Starting backup job"
if ! /usr/local/bin/do-backup; then
log "Backup failed"
exit 1
fi
log "Backup finished successfully"
Тогда в systemd любой ненулевой код будет отмечен как failed, что легко ловить мониторингом.

Типовые шаблоны миграции cron → systemd timers
Ежедневные ночные задания
Пример записи в crontab:
0 3 * * * /usr/local/bin/nightly-report.sh
Эквивалент в таймере:
[Timer]
Persistent=true
Можно добавить RandomizedDelaySec=300, чтобы чуть разнести старт, если на сервере много задач в 03:00.
«Каждые N минут/часов»
Примеры cron:
*/5 * * * * /usr/local/bin/ping-api.sh
0 */2 * * * /usr/local/bin/cleanup-temp.sh
Через OnCalendar:
[Timer]
или через монотонные таймеры:
[Timer]
AccuracySec=30s
Монотонные таймеры удобны, когда неважно, стартует ли задача ровно на отметке, главное — интервал между завершением и следующим запуском.
Гибкость OnCalendar против формата cron
Формат OnCalendar мощнее и при этом достаточно читаемый. Примеры:
OnCalendar=*-*-* 03:00— каждый день в 03:00.OnCalendar=Mon..Fri 01:30— по будням в 01:30.OnCalendar=*-01-01 00:00— каждое 1 января.OnCalendar=*:0/15— каждые 15 минут.
Понять, что именно означает выражение, можно через:
systemd-analyze calendar "Mon..Fri 01:30"
Это удобный способ проверить расписание до того, как вы примените таймер.
Параллельные запуски и блокировки
Одна из самых частых реальных проблем периодических задач — ситуации, когда новый запуск начинается до завершения предыдущего. В cron это типичная причина «просадки» сервера и гонок данных.
Варианты решения:
- Жёсткий
RuntimeMaxSecв юните сервиса — задача будет убита по таймауту. - Использование
flockвнутри скрипта, чтобы не допускать параллельных запусков. - Мониторинг по времени выполнения (если превышено — алерт).
Пример настройки таймаута:
[Service]
Type=oneshot
ExecStart=/usr/local/bin/long-job.sh
RuntimeMaxSec=900
И пример скрипта с flock (да, это не зависит от systemd, но хорошо комбинируется):
#!/bin/bash
set -euo pipefail
LOCKFILE=/run/long-job.lock
exec 9>"$LOCKFILE"
if ! flock -n 9; then
echo "$(date -Is) Another instance is running, exiting"
exit 0
fi
# Дальше безопасный код, параллельных инстансов не будет
Сравнение cron и systemd timers для healthcheck
С точки зрения наблюдаемости:
- Cron: статус разбросан по логам,
maillog, собственным логам скриптов; нужна ручная интеграция с мониторингом. - Systemd timers: единый статус юнита, готовые поля о последнем запуске, exit-коде, длительности; логи в journald.
Для построения healthcheck обычно достаточно поверх systemd добавить тонкий слой:
- Скрипт или экспортёр, который смотрит
systemctl showнужных юнитов. - Алерт, если, скажем, за последние 2 запуска хотя бы один закончился статусом
failedили между успешными запусками прошло больше X минут или часов.
Где cron остаётся безальтернативным
Есть сценарии, где systemd timers недоступны или избыточны:
- Ограниченный shared-хостинг без доступа к
systemd, где можно только редактироватьcrontabчерез панель. - Контейнеры и минимальные образы, где
systemdсознательно не используется (поднимается только один процесс). - Легаси-скрипты и пакеты, которые обновлять рискованно, а вы не хотите затрагивать штатный
cronдистрибутива.
В таких случаях лучше «обвешать» cron теми же практиками: flock, явное логирование, нормальные exit-коды, внешние healthcheck-сервисы.
Практическая стратегия: комбинировать, а не воевать
В реальных инфраструктурах часто удобнее не устраивать религиозную войну «cron vs systemd timers», а распределить зоны ответственности:
- Системные задачи дистрибутива — пусть остаются, как есть (часть в cron, часть уже в таймерах).
- Все ваши продакшен-критичные задания — постепенно переводить в
systemd timersс чётко описанными юнитами, логами и лимитами. - На shared и минимальных окружениях — аккуратно продолжать использовать cron, но по тем же принципам: явные таймауты, логика ретраев, healthcheck.
Подход «новые задачи только через таймеры» и постепенный перенос старых по мере доработок — самый безболезненный.
Выводы
Классический cron никуда не делся и вполне пригоден для простых сценариев или ограниченных окружений. Но там, где вы контролируете систему и уже используете systemd, systemd timers дают слишком много преимуществ, чтобы их игнорировать:
- Чёткая связь задачи и сервиса.
- Прозрачный статус и логи в journald.
- Лимиты по ресурсам, безопасность, зависимости, sandbox.
- Более удобный
healthcheckи интеграция с мониторингом.
Если ваши бэкапы, регенерация кэшей, интеграции с внешними API, почтовые рассылки и прочие периодические задачи до сих пор живут в «темноте» cron без статуса и мониторинга — самое время переписать их на systemd timers и наконец-то увидеть, что они реально делают.


