Cron has scheduled jobs on Unix for decades. On systemd systems, timers do the same job with extra benefits: logs in the journal, dependency handling, catching up on runs missed while the server was off, random delays to avoid thundering herds, and resource limits. Your distribution already uses them for apt-daily, logrotate and fstrim.
See existing timers
systemctl list-timers --all
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2026-10-15 00:00:00 UTC 9h left Wed 2026-10-14 00:00:02 UTC 14h ago logrotate.timer logrotate.service
Thu 2026-10-15 06:12:31 UTC 15h left Wed 2026-10-14 06:40:11 UTC 8h ago apt-daily-upgrade.timer apt-daily-upgrade.service
A timer needs two units
- A service that defines what runs.
- A timer that defines when.
Example: a nightly database backup.
# /etc/systemd/system/db-backup.service
[Unit]
Description=Nightly MySQL backup
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/db-backup.sh
Nice=10
IOSchedulingClass=idle
# /etc/systemd/system/db-backup.timer
[Unit]
Description=Run db-backup nightly
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
RandomizedDelaySec=10m
[Install]
WantedBy=timers.target
Enable the timer, not the service:
sudo systemctl daemon-reload
sudo systemctl enable --now db-backup.timer
systemctl list-timers db-backup.timer
Run the job right now for testing:
sudo systemctl start db-backup.service
journalctl -u db-backup.service -n 50
OnCalendar syntax
The format is DayOfWeek Year-Month-Day Hour:Minute:Second, where * means “any”:
| Schedule | OnCalendar |
|---|---|
| Every day at 03:15 | *-*-* 03:15:00 or 03:15 |
| Every hour | hourly (*-*-* *:00:00) |
| Every 15 minutes | *:0/15 |
| Weekdays at 08:00 | Mon..Fri 08:00 |
| Sundays at 02:00 | Sun 02:00 |
| First day of the month | *-*-01 00:00 or monthly |
| Twice a day | *-*-* 06,18:00 |
| In a time zone | *-*-* 03:00 Europe/Berlin |
Check an expression before using it:
systemd-analyze calendar '*:0/15'
systemd-analyze calendar --iterations=3 'Mon..Fri 08:00'
Relative timers (monotonic)
Instead of wall-clock times, run relative to events:
[Timer]
OnBootSec=5min # 5 minutes after boot
OnUnitActiveSec=1h # then every hour after the last run
This is useful for “every N minutes” jobs where the exact clock time doesn’t matter.
Useful options
Persistent=true— if the server was off at the scheduled time, run the job at the next boot. Cron would simply skip it.RandomizedDelaySec=10m— spread start times, so many servers don’t hit a backup target or API at the same second.AccuracySec=1s— by default, timers may fire up to 1 minute late to save wake-ups. Tighten it if precision matters.Type=oneshotin the service — systemd waits for the script to finish, and a timer never starts a second copy while one is still running. Overlap protection is built in, unlike cron.
Logs and failures
Everything the job prints goes to the journal:
journalctl -u db-backup.service --since today
systemctl status db-backup.service # last result, exit code
systemctl list-units --failed
Get notified on failure with OnFailure= in the service:
[Unit]
OnFailure=notify-failure@%n.service
[email protected] is a small template unit you write that sends an email or webhook.
Resource limits and security
Because the job is a regular service, all systemd settings apply:
[Service]
MemoryMax=512M
CPUQuota=50%
ProtectSystem=strict
ReadWritePaths=/var/backups
PrivateTmp=true
See systemd unit files.
Timers for a user (no root)
Put the units in ~/.config/systemd/user/ and use systemctl --user enable --now name.timer. To keep them running when you’re logged out, run sudo loginctl enable-linger $USER.
Timers vs cron
| cron | systemd timers | |
|---|---|---|
| Setup | One line | Two small files |
| Logs | Mail or your own redirect | journald, automatically |
| Missed runs | Skipped | Persistent=true catches up |
| Overlap protection | Manual (flock) |
Built in for oneshot services |
| Random delay | Manual | RandomizedDelaySec |
| Resource limits, sandboxing | No | Yes |
| Portability | Everywhere | systemd systems only |
Cron is still perfectly fine for quick, simple jobs. See crontab. Use timers for important jobs where logging, catch-up and limits matter.
Quick reference
| Task | Command |
|---|---|
| List timers | systemctl list-timers --all |
| Enable | systemctl enable --now name.timer |
| Run job now | systemctl start name.service |
| Logs | journalctl -u name.service |
| Test schedule | systemd-analyze calendar 'expr' |
| Disable | systemctl disable --now name.timer |
Related: cron expressions and database backup automation.