Cron has run scheduled jobs on Unix systems for decades, and there's a reasonable argument for never touching it given how well-understood and stable it is. I switched most of my scheduled jobs to systemd timers anyway, mainly for two reasons: built-in logging and explicit dependency handling, both of which cron has always lacked.

Where cron starts to creak

Cron's biggest practical limitation is observability. By default, a cron job's output goes to local mail delivery, which on most modern servers either isn't configured or gets routed somewhere nobody checks. Finding out a job failed three nights ago means digging through whatever ad-hoc logging the script itself happened to include.

The anatomy of a systemd timer

A systemd timer is always paired with a systemd service — the timer defines when something runs, the service defines what runs. This separation is more verbose than a single crontab line, but it means the same dependency and resource-control mechanisms used for every other systemd service apply to scheduled jobs too.

/etc/systemd/system/db-backup.service

[Unit]
Description=Nightly database backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/run-backup.sh
User=backup

/etc/systemd/system/db-backup.timer

[Unit]
Description=Run db-backup nightly at 02:30

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true

[Install]
WantedBy=timers.target

Persistent=true is one of the small wins over cron — if the server is down at 02:30 for any reason, the job runs as soon as the system is back up, rather than silently skipping that day entirely.

Logging that doesn't require a separate mail setup

Every systemd service's output goes to the journal automatically, queryable with standard tools rather than depending on whatever the script author thought to log:

journalctl -u db-backup.service --since "yesterday"

Checking whether last night's backup succeeded is one command, with full stdout and stderr captured regardless of whether the script itself logs anything. With cron, that same question often means SSHing in and grepping through a log file the script may or may not have written correctly.

Dependency handling cron can't do

The other real advantage is expressing dependencies declaratively. A backup job that needs the database to be fully up can declare that directly:

[Unit]
Requires=postgresql.service
After=postgresql.service

Cron has no concept of this — a cron job scheduled for 2:30am runs at 2:30am regardless of whether anything it depends on is actually ready, which has caused more than one "the backup ran but captured an empty database" incident across servers I've inherited from other admins.

The rough edges

None of this is a strictly better replacement in every way. A few things cron does that take more effort in systemd:

  • Verbosity. Two files instead of one crontab line is more ceremony for genuinely trivial jobs, and it adds up across a server with a dozen small scheduled tasks.
  • OnCalendar syntax. Systemd's calendar expression format is more powerful than cron's five-field syntax, but it's also less universally known — I still double check the man page for anything beyond a simple daily or weekly schedule.
  • Portability. A crontab is trivially portable across any Unix-like system. systemd timers are Linux-specific and tied to a particular init system, which matters if you ever need to support BSD or non-systemd distributions.

For servers I fully control, running a modern systemd-based distribution, the switch has been a clear net positive — mostly for the logging alone. For anything that needs to stay portable across init systems, cron remains the right, boring choice.

ML

Mikko Laaksonen

DevOps engineer based in Tampere, Finland. Writes about Linux, Docker, databases, and web design.

More about Mikko →