Skip to content
SumGuy's Ramblings
Go back

Systemd Timers vs Cron: Scheduling Tasks Like It's Not 1995

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

FeatureCronSystemd Timers
LoggingManual (redirect to file)Automatic (journald)
Dependency managementNoneFull systemd dependency graph
Persistent missed runsNoYes (with Persistent=true)
Run on bootHacky (@reboot + sleep)OnActiveSec= after boot
User-level schedulingYesYes
Run as specific userSpecify in /etc/cron.d/User= in service unit
Syntax complexityOne line (weird but compact)Two files (verbose but readable)
Simultaneous run preventionManual (use lockfiles)Type=oneshot prevents it
UniversalityEvery Unix systemLinux with systemd only
Debugging failed runsCheck log file (if set up)journalctl -u service
Resource limitsNoneFull cgroup limits

When to Use Each

Use cron when:

Use systemd timers when:

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.


Share this post on:

Previous Post
Suricata vs Snort: Intrusion Detection for the Paranoid Home Lab Owner
Next Post
Immich vs PhotoPrism: Self-Hosted Google Photos That Won't Sell Your Memories