К 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, иначе таймер тихо перестанет срабатывать навсегда.