I used cron for years without really questioning it. It works, it’s everywhere, it’s well-understood. But over time I noticed a few recurring frustrations: no built-in logging, missed jobs when the system was off, no dependency management, and debugging failed jobs meant hunting through syslog. Systemd timers solve most of these problems, and on systems already running systemd (which is basically every modern Linux distro now), they’re a natural fit.
This isn’t about replacing cron everywhere — cron is simpler for many things. But for scheduled tasks you actually care about, systemd timers are worth knowing.
The Basic Concept
Systemd timers come in pairs: a .timer unit and a .service unit. The timer defines the schedule; the service defines what to run. The service can also be triggered manually without the timer, which is useful for testing.
A Simple Example: Daily Backup
Let’s say I have a backup script at /usr/local/bin/backup.sh. I want it to run every day at 2am.
First, the service unit at /etc/systemd/system/backup.service:
[Unit]
Description=Daily backup script
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=root
StandardOutput=journal
StandardError=journal
Type=oneshot — the service runs once and exits. Appropriate for scripts and tasks as opposed to daemons.StandardOutput=journal / StandardError=journal — output goes to systemd journal, so you can view it later with journalctl.
Now the timer unit at /etc/systemd/system/backup.timer:
[Unit]
Description=Run backup daily at 2am
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
OnCalendar=*-*-* 02:00:00 — run every day at 2am.Persistent=true — if the system was off at 2am, run the job at the next possible time instead of skipping it.
Enable and start:
systemctl daemon-reload
systemctl enable backup.timer
systemctl start backup.timer
Check status:
systemctl status backup.timer
OnCalendar Syntax
The time specification format is flexible. Some examples:
OnCalendar=daily # Every day at midnight
OnCalendar=hourly # Every hour
OnCalendar=weekly # Every Monday at midnight
OnCalendar=monthly # First day of each month at midnight
OnCalendar=*-*-* 02:00:00 # Every day at 2am
OnCalendar=Mon *-*-* 06:00:00 # Every Monday at 6am
OnCalendar=*-*-1 00:00:00 # First day of every month
OnCalendar=*:0/15 # Every 15 minutes
OnCalendar=2024-12-25 00:00:00 # Specific date/time
To verify a time expression:
systemd-analyze calendar "Mon *-*-* 06:00:00"
This shows when the next activation will be, which is helpful for debugging.
OnBootSec and OnActiveSec
Timers don’t have to use calendar time. They can fire relative to system events:
[Timer]
OnBootSec=5min
OnUnitActiveSec=1h
OnBootSec=5min — run 5 minutes after boot.OnUnitActiveSec=1h — run 1 hour after the timer last activated.
This is useful for things like cache warming, health checks, or cleanup tasks that should run periodically without being tied to clock time.
Viewing Timer Status
List all active timers:
systemctl list-timers
Output shows each timer, when it last ran, when it will next run, and the service it triggers. This is immediately more informative than crontab -l.
Check logs for a specific service:
journalctl -u backup.service
journalctl -u backup.service --since "yesterday"
journalctl -u backup.service -n 50 # last 50 lines
This is a big deal. With cron, output goes to email (if configured) or gets lost. With systemd, it’s all in the journal with timestamps and exit codes.
Running a Timer Job Manually
Because the service and timer are separate units, you can run the job on demand:
systemctl start backup.service
Then check if it ran correctly:
systemctl status backup.service
journalctl -u backup.service -n 30
This is great for testing new scripts before enabling the timer. You run it manually, check the output, fix problems, then enable the timer once you’re confident it works.
Monitoring Failures
Add OnFailure to notify when a job fails:
[Unit]
Description=Daily backup script
OnFailure=notify-failure@%n.service
Create a notification service /etc/systemd/system/notify-failure@.service:
[Unit]
Description=Send failure notification
[Service]
Type=oneshot
ExecStart=/usr/local/bin/notify-failure.sh %i
And a simple notification script /usr/local/bin/notify-failure.sh:
#!/bin/bash
echo "Service $1 failed at $(date)" | mail -s "Systemd failure: $1" you@example.com
Or use a simpler approach with the FailureAction or sending a webhook.
User-Level Timers
Systemd timers can run as a regular user without root. Place units in ~/.config/systemd/user/:
mkdir -p ~/.config/systemd/user/
Create ~/.config/systemd/user/sync.timer and ~/.config/systemd/user/sync.service, then:
systemctl --user daemon-reload
systemctl --user enable sync.timer
systemctl --user start sync.timer
systemctl --user list-timers
User timers run when the user is logged in (by default). For persistent user timers that run even without an active login session, enable linger:
loginctl enable-linger username
Useful Patterns
Run a script with a timeout:
[Service]
Type=oneshot
ExecStart=/usr/local/bin/task.sh
TimeoutStartSec=300
Kills the service if it runs longer than 5 minutes.
Environment variables:
[Service]
Type=oneshot
Environment="DB_HOST=localhost"
Environment="DB_NAME=mydb"
EnvironmentFile=/etc/backup.env
ExecStart=/usr/local/bin/backup.sh
Randomize start time (avoid thundering herd):
[Timer]
OnCalendar=daily
RandomizedDelaySec=3600
Adds a random delay up to 1 hour. Useful when many servers run the same timer — spreads load rather than everyone hitting a shared resource at exactly midnight.
When Cron is Still Better
I still use cron for simple, one-line tasks where the overhead of creating two files isn’t worth it. */5 * * * * /usr/bin/check-something.sh in a crontab is faster to set up than creating a .timer and .service file.
For anything where I care about:
- Logging what happened
- Catching up missed runs
- Knowing definitively when the last run was
- Easily triggering a manual run
- Dependencies (run after network.target)
…systemd timers are the right choice.
The learning curve is small but real — the configuration is more verbose than a crontab line. But the operational visibility you gain is worth it for important tasks. Being able to run systemctl list-timers and see exactly when everything last ran and when it’s next scheduled is genuinely useful.