К cron у меня нет претензий идеологического характера: он работает и никуда не денется. Но на серверах, где уже есть systemd, таймеры дают три вещи бесплатно.
Состояние видно без чтения логов
systemctl list-timers показывает, когда задача запускалась и когда запустится в следующий раз. С cron для этого нужно держать в голове crontab -l и верить, что последний запуск вообще был.
$ systemctl list-timers --all | grep backup
Tue 2026-01-13 03:20:00 MSK 8h left Sun 2026-01-12 03:20:11 MSK 15h ago backup.timer backup.service
Пропущенный запуск не теряется
Если сервер был выключен в момент запуска, cron молча пропускает задачу. Таймер с Persistent=true догоняет её при загрузке — для ночных бэкапов и ротации это ровно то поведение, которое нужно.
Юнит — это файл, а не строка
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly database backup
After=network-online.target postgresql.service
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/backup.sh
Nice=10
IOSchedulingClass=idle
TimeoutStartSec=2h
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup nightly
[Timer]
OnCalendar=*-*-* 03:20:00
Persistent=true
RandomizedDelaySec=20m
[Install]
WantedBy=timers.target
RandomizedDelaySec — недооценённая строка. Двадцать минут разброса спасают от ситуации, когда десять серверов начинают бэкап в одну и ту же секунду и забивают канал.
Где cron всё ещё проще
Разовые задания на пользователя и всё, что живёт вне системы: @reboot-скрипты, задачи в контейнерах без systemd, машины, где systemd нет вообще. Переписывать их ради единообразия смысла нет.
Ещё одна деталь: таймер не запускает следующий экземпляр, пока работает предыдущий. Для бэкапа это защита от наложения, но если задача может зависнуть — ставьте TimeoutStartSec, иначе таймер тихо перестанет срабатывать навсегда.