Cron Has Been Working Fine Since Before You Were Born
Cron is older than Linux itself. It’s been scheduling tasks reliably on Unix systems since 1975, which means it’s been solving the “run this thing at 3am” problem for longer than most of its users have been alive. That kind of staying power means something.
Every sysadmin knows cron. Every Linux distribution ships it. The syntax is weird (30 2 * * * means “2:30am daily” and you will never intuit that) but there are approximately 400 websites that will translate it for you. It works, it’s simple, and it’s everywhere.
So why does systemd offer an alternative?
Because cron has some genuinely annoying failure modes that have bitten people badly enough to invent something new.
What Cron Does Right
Let’s be fair to the veteran. Cron gets a lot of things right:
Simplicity. A crontab entry is one line. There’s no unit file, no dependency graph, no companion service file. You type crontab -e, write the line, save, and you’re done.
Universality. Cron works on every Linux, BSD, macOS, and any Unix-like system. If you’re writing a script that needs to run on heterogeneous infrastructure, cron is the common denominator.
User-level scheduling. Any user can have their own crontab without root access. Your personal backup script, your user-level maintenance tasks — no privilege escalation required.
Low ceremony. It starts automatically, runs in the background, and requires zero configuration beyond the crontab itself.
A Basic Cron Example
# Edit your crontab
crontab -e
# Run a backup script every day at 2:30am
30 2 * * * /home/user/scripts/backup.sh
# Run a cleanup script every Sunday at midnight
0 0 * * 0 /home/user/scripts/weekly-cleanup.sh
# Run every 15 minutes
*/15 * * * * /usr/local/bin/health-check.sh
The /etc/cron.d/ directory works for system-level jobs:
# /etc/cron.d/my-backup
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# Run as www-data user at 3am daily
0 3 * * * www-data /opt/app/scripts/backup.sh >> /var/log/backup.log 2>&1
Note the >> /var/log/backup.log 2>&1 at the end. This is you solving cron’s logging problem by hand, which is a hint about what’s coming.
What Cron Does Wrong
The Logging Problem
Cron has no built-in logging. Your script runs. Did it succeed? Did it fail? Did it produce output? By default: no idea.
Cron will email output to the local root mailbox if the job produces any stdout/stderr, but most modern servers aren’t running a mail daemon, so those emails vanish into /dev/null quietly. You end up with two options: redirect output to a log file manually (remember that >> /var/log/backup.log 2>&1 trick), or stay blissfully ignorant that your backup has been failing for weeks.
The “Starts at Boot” Problem
Cron has no “run this on boot” or “run this after a service starts” capability. Want to run a script five minutes after the system boots? You’re combining cron with @reboot, sleep commands, and hope:
# Fragile! Race condition if system takes longer to boot than expected
@reboot sleep 300 && /usr/local/bin/post-boot-init.sh
The Dependency Problem
Cron doesn’t know about other services. It doesn’t know if your database is running, if your network interface is up, or if a previous run is still executing. Run a slow job every 5 minutes and have one take 7 minutes? Now two instances are running simultaneously, and depending on what that script does, you might have a bad time.
Runs as Root by Default
System cron jobs in /etc/crontab or /etc/cron.d/ run as root unless you explicitly specify a user. This is a larger attack surface than necessary. Writing a bug in your cron script can mean accidentally deleting /etc instead of /tmp/cache.
Enter Systemd Timers
Systemd timers are the modern alternative. They work alongside systemd’s service management instead of running as a separate daemon. Every timer has a companion .service file that defines what actually runs — the timer just controls when it runs.
This means your scheduled task gets all of systemd’s benefits: logging via journald, dependency management, resource limits, user-level execution without privilege issues, and restart policies.
The .service + .timer Pair
A systemd timer requires two files:
The service unit (/etc/systemd/system/backup.service):
[Unit]
Description=Daily backup script
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/backup.sh
StandardOutput=journal
StandardError=journal
The timer unit (/etc/systemd/system/backup.timer):
[Unit]
Description=Run backup daily at 2:30am
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
Enable and start the timer (not the service):
systemctl daemon-reload
systemctl enable --now backup.timer
The Persistent=true line means if the system was off during the scheduled time, the timer will fire once when the system comes back up. This is a huge improvement over cron, which simply skips missed runs.
OnCalendar Syntax
Systemd’s calendar syntax is more readable than cron’s:
# Every day at 2:30am
OnCalendar=*-*-* 02:30:00
# Every hour
OnCalendar=hourly
# Every 15 minutes
OnCalendar=*:0/15
# Every Sunday at midnight
OnCalendar=Sun *-*-* 00:00:00
# First day of every month at noon
OnCalendar=*-*-01 12:00:00
# Every weekday at 9am
OnCalendar=Mon..Fri *-*-* 09:00:00
Test your calendar expression without creating files:
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"
This is genuinely useful — you can verify the schedule before deploying it.
Logging with journalctl
Every run of a systemd timer is automatically logged to the journal:
# See all runs of the backup service
journalctl -u backup.service
# See the last 20 lines
journalctl -u backup.service -n 20
# Follow logs in real time (useful during testing)
journalctl -u backup.service -f
# See logs from the last run only
journalctl -u backup.service --since "1 hour ago"
No log files to maintain, no >> /var/log/backup.log 2>&1 appended to every command. It just works.
Running as a Non-Root User
User-level timers run without any root involvement:
# Place files in user's systemd config directory
mkdir -p ~/.config/systemd/user/
# ~/.config/systemd/user/my-backup.service
[Unit]
Description=My personal backup
[Service]
Type=oneshot
ExecStart=%h/scripts/backup.sh
# ~/.config/systemd/user/my-backup.timer
[Unit]
Description=Daily personal backup timer
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
# Enable and start (no sudo)
systemctl --user daemon-reload
systemctl --user enable --now my-backup.timer
# Check status (no sudo)
systemctl --user status my-backup.timer
journalctl --user -u my-backup.service
Dependency Awareness
The After= and Wants= directives in the service unit mean your task won’t run until its dependencies are satisfied:
[Unit]
Description=Database maintenance
After=postgresql.service
Requires=postgresql.service
This is impossible with cron. You could write a shell script that polls for the database to be ready, but that’s engineering around cron’s limitations rather than using a proper dependency system.
Cron vs Systemd Timers: The Comparison
| Feature | Cron | Systemd Timers |
|---|---|---|
| Logging | Manual (redirect to file) | Automatic (journald) |
| Dependency management | None | Full systemd dependency graph |
| Persistent missed runs | No | Yes (with Persistent=true) |
| Run on boot | Hacky (@reboot + sleep) | OnActiveSec= after boot |
| User-level scheduling | Yes | Yes |
| Run as specific user | Specify in /etc/cron.d/ | User= in service unit |
| Syntax complexity | One line (weird but compact) | Two files (verbose but readable) |
| Simultaneous run prevention | Manual (use lockfiles) | Type=oneshot prevents it |
| Universality | Every Unix system | Linux with systemd only |
| Debugging failed runs | Check log file (if set up) | journalctl -u service |
| Resource limits | None | Full cgroup limits |
When to Use Each
Use cron when:
- You need portability across non-systemd systems (BSD, macOS, older Linux)
- The task is simple and failure consequences are low
- You’re managing a user’s personal tasks on a shared server
- You want the minimum ceremony possible
Use systemd timers when:
- You need reliable logging for audit or debugging
- The task has service dependencies (needs network up, needs database running)
- You care about what happens when the system was off during the scheduled time
- You want to apply resource limits to the task
- You’re replacing cron for anything that runs on a modern systemd Linux server
For most production Linux servers running modern distributions, systemd timers are the better default for new scheduled tasks. The two-file overhead compared to one cron line is real, but you pay for it with actual visibility into whether your tasks are running successfully. Finding out your backup script has been silently failing for a month because a log redirect was wrong is a significantly worse experience than writing a .service file.