Cron has been scheduling Linux tasks since 1975. systemd timers arrived around 2010 and are now the default on every major Linux distribution. So which should you actually use?
The honest answer: cron is still great for simple tasks, but systemd timers win on every objective measure for production services. Here's the full comparison.
| Feature | Cron | systemd timer |
|---|---|---|
| Setup complexity | โ One line in crontab | Two files (.timer + .service) |
| Logging | Email or redirect manually | โ Full journald integration |
| Dependencies | None | โ After=, Requires=, etc. |
| Catch up on missed runs | โ Missed runs are lost | โ Persistent= option |
| Run on boot + interval | Need @reboot + another line | โ OnBootSec= + OnUnitActiveSec= |
| Randomised delay | โ Not supported | โ RandomizedDelaySec= |
| Security sandboxing | Limited | โ Full systemd sandboxing |
| Familiar syntax | โ Universally known | Different syntax to learn |
| Per-user scheduling | โ crontab -e | โ User timers (more complex) |
| Works on all Unix systems | โ macOS, BSDs, Solaris... | Linux only |
The same schedule expressed in cron and systemd:
0 9 * * 1-5 /path/to/script.sh
[Unit]
Description=My task timer
[Timer]
OnCalendar=Mon..Fri *-*-* 09:00:00
Persistent=true
[Install]
WantedBy=timers.target
*/5 * * * * /path/to/script.sh
[Timer]
OnBootSec=5min
OnUnitActiveSec=5min
0 0 1 * * /path/to/script.sh
[Timer]
OnCalendar=*-*-01 00:00:00
Persistent=true
Validate cron expressions before converting them. Paste into CronBuilder.dev to confirm the schedule is correct โ then translate to systemd syntax.
With cron, output handling is your problem. You have to explicitly redirect stdout and stderr to a log file, rotate those logs manually, and grep through them to find what you want.
systemd timers write all output to journald automatically. You get structured, indexed, rotatable logs with zero configuration:
# View logs for a specific timer โ the last 50 lines
journalctl -u mytask.service -n 50
# Follow logs in real time
journalctl -u mytask.service -f
# See logs from the last run only
journalctl -u mytask.service --since "$(systemctl show mytask.service --value -p ExecMainStartTimestamp)"
# See all timer run history with exit codes
systemctl status mytask.timer
With cron, the equivalent requires setting up >> /var/log/mytask.log 2>&1, configuring logrotate, and writing your own grep query. systemd gives you all of this for free.
One of cron's biggest gaps is that it knows nothing about the state of the system. If you schedule a database backup at 2am and the database service is down, cron runs the script anyway and it fails silently.
systemd timers can declare dependencies:
# mytask.service
[Unit]
Description=Database backup
# Wait for the network and the database to be available
After=network-online.target postgresql.service
Requires=postgresql.service
[Service]
Type=oneshot
User=postgres
ExecStart=/usr/local/bin/backup.sh
With this configuration, the backup never runs unless PostgreSQL is actually available. If PostgreSQL is down, the timer fires but the service fails immediately with a clear dependency error in the journal โ no mystery failures.
Here's a scenario: your server reboots at 1:57am. Your cron job was scheduled for 2:00am. Cron will miss the run because it wasn't running at 2am. There's no mechanism to catch up.
systemd's Persistent=true option records the last run time and, on the next boot, immediately runs the timer if the scheduled time has passed since the last run:
[Timer]
OnCalendar=*-*-* 02:00:00
# Run immediately on next boot if the 2am run was missed
Persistent=true
This is invaluable for nightly backups, certificate renewal jobs, and any task where missing a run has consequences.
A systemd timer requires two files: a .service file that defines what to run, and a .timer file that defines when to run it. Both go in /etc/systemd/system/ for system-level jobs, or ~/.config/systemd/user/ for user-level jobs.
# /etc/systemd/system/mybackup.service
[Unit]
Description=My backup script
After=network.target
[Service]
Type=oneshot
User=backup-user
ExecStart=/usr/local/bin/backup.sh
StandardOutput=journal
StandardError=journal
# /etc/systemd/system/mybackup.timer
[Unit]
Description=Run backup daily at 2am
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
# Randomise start by up to 1 minute to avoid thundering herd
RandomizedDelaySec=60
[Install]
WantedBy=timers.target
# Reload systemd to pick up the new units
sudo systemctl daemon-reload
# Enable and start the timer
sudo systemctl enable --now mybackup.timer
# Verify it's running and see the next scheduled run
systemctl status mybackup.timer
# List all active timers
systemctl list-timers
The practical upshot: new production services on modern Linux should default to systemd timers. For personal scripts and anything where you need portability or quick setup, cron is still perfectly fine. The two tools coexist happily โ you don't have to pick one for everything.
And wherever you're scheduling, it starts with getting the expression right. The CronBuilder tool validates cron syntax and shows your next 10 run times โ so you can confirm the schedule before you deploy it, regardless of whether you're writing a crontab line or a systemd OnCalendar value.
Whether you're writing a cron expression or translating one to systemd OnCalendar format, use CronBuilder.dev to confirm the timing looks right before you deploy.
Build & Validate โRelated: Cron job not running? โ 10 reasons and fixes. GitHub Actions cron schedule โ UTC, 5-min minimum, and gotchas.