Why I Moved Some Cron Jobs to systemd Timers
I’ve been running cron jobs on my home servers for years. They handle backups, log rotation, and periodic cleanup tasks. For most of these, cron works fine. But I kept running into the same problem: jobs would fail silently because a dependency wasn’t ready yet.
For example, I have a backup script that copies files to my Synology NAS. If the NAS mount isn’t ready when the cron job fires after a reboot, the backup fails. The script writes to a log file, but I have to remember to check it. By the time I notice, I’ve missed several backup cycles.
I started looking at systemd timers because they let me define dependencies explicitly. If my backup needs the NAS mounted first, I can tell systemd to wait for that. I also wanted better logging without having to manage separate log files for every script.
My Real Setup
I run Proxmox on my main home server. Most of my automation runs in LXC containers, but some tasks need to run directly on the host. I picked a simple task to test systemd timers: a script that checks disk usage and sends me a notification if any partition is over 85% full.
The script itself is straightforward:
#!/usr/bin/env bash
df -h | awk '$5 ~ /^[0-9]+%$/ { sub(/%/, "", $5); if ($5 > 85) print $0 }' |
while read line; do
echo "Disk usage warning: $line" | mail -s "Disk Alert" [email protected]
done
I saved this as /usr/local/bin/check-disk.sh and made it executable. Previously, this ran via cron every hour. It worked, but if the mail service wasn’t up yet after a reboot, the notification would fail.
Creating the Service Unit
Systemd separates the “what” from the “when.” The service unit defines what runs. I created /etc/systemd/system/check-disk.service:
[Unit]
Description=Check disk usage and alert if high
After=network.target postfix.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/check-disk.sh
User=root
StandardOutput=journal
StandardError=journal
The After= line is what I couldn’t do with cron. This tells systemd not to run the script until both the network is up and Postfix (my mail service) is running. The Type=oneshot means the service runs once and exits, which is what I want for a scheduled task.
I set StandardOutput=journal so all output goes directly to systemd’s journal instead of a separate log file.
Creating the Timer Unit
The timer unit defines when the service runs. I created /etc/systemd/system/check-disk.timer:
[Unit]
Description=Run disk check hourly
[Timer]
OnCalendar=hourly
Persistent=true
[Install]
WantedBy=timers.target
The OnCalendar=hourly line runs the job at the top of every hour. The Persistent=true setting is important: if the system is down when a scheduled run should happen, systemd will run it once when the system comes back up. Cron just skips missed runs.
Enabling and Testing
After creating both files, I reloaded systemd and enabled the timer:
sudo systemctl daemon-reload
sudo systemctl enable check-disk.timer
sudo systemctl start check-disk.timer
To verify it was scheduled correctly:
systemctl list-timers check-disk.timer
This showed me when it last ran and when it would run next. To see the actual output from the last run:
journalctl -u check-disk.service
This is where systemd timers felt immediately better than cron. I didn’t have to hunt for a log file or remember where I configured logging. Everything was in the journal.
What Worked
The dependency management worked exactly as I hoped. I rebooted the server and watched the logs. The timer waited until Postfix was fully up before running the disk check. When I tested with the network cable unplugged, it waited for that too.
The persistent state tracking also proved useful. I shut down the server for maintenance during a scheduled run time. When I brought it back up, systemd ran the missed check automatically. With cron, I would have just skipped that check entirely.
Logging through journalctl is cleaner than managing separate log files. I can filter by service, by time range, or follow logs in real time. For debugging, this is much faster than grepping through text files.
What Didn’t Work
The syntax for OnCalendar is more complex than cron’s. Cron’s five-field format is burned into my muscle memory. Systemd’s calendar expressions are more powerful, but I had to look up the syntax every time at first. For example, running something every 15 minutes is OnCalendar=*:0/15, which is less intuitive than cron’s */15 * * * *.
I also ran into an issue with environment variables. My script relied on PATH being set a certain way, which worked in cron because it inherited my user’s environment. With systemd, I had to explicitly set environment variables in the service unit or use absolute paths everywhere.
The two-file approach (service + timer) feels heavier than a single cron entry. For simple tasks that don’t need dependencies or special logging, cron is still faster to set up.
When I Still Use Cron
I haven’t converted all my cron jobs to systemd timers. I still use cron for:
- Simple tasks with no dependencies (like clearing temp files)
- Jobs that run in user space, not system-wide
- Quick one-off scheduled tasks I’ll probably delete soon
Cron is lighter and faster to configure. If I just need something to run at 3 AM every day and I don’t care about logging or dependencies, cron is still my first choice.
Key Takeaways
Systemd timers are worth using when you need dependency management or better logging. The persistent state tracking is a real advantage for systems that don’t run 24/7.
The learning curve is steeper than cron, especially for the calendar syntax. But once you have a few working examples, it becomes routine.
I now use systemd timers for any task that needs to wait for other services or where I want detailed logs without managing separate files. For everything else, cron is still simpler and perfectly adequate.