// linux ยท scheduling

Cron vs systemd Timers:
Which Should You Use in 2026?

๐Ÿ“… March 2026 โฑ 8 min read ๐Ÿท Linux ยท systemd ยท DevOps

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.

// contents
  1. Feature comparison
  2. Syntax side-by-side
  3. Logging and observability
  4. Dependencies and ordering
  5. Handling missed runs
  6. Setting up a systemd timer from scratch
  7. When to use each

Feature 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

Syntax Side-by-Side

The same schedule expressed in cron and systemd:

"Run every weekday at 9am"

cron
0 9 * * 1-5 /path/to/script.sh
systemd โ€” mytask.timer
[Unit]
Description=My task timer

[Timer]
OnCalendar=Mon..Fri *-*-* 09:00:00
Persistent=true

[Install]
WantedBy=timers.target

"Run every 5 minutes"

cron
*/5 * * * * /path/to/script.sh
systemd โ€” mytask.timer
[Timer]
OnBootSec=5min
OnUnitActiveSec=5min

"Run on the 1st of every month"

cron
0 0 1 * * /path/to/script.sh
systemd โ€” mytask.timer
[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.

Logging: systemd Wins Decisively

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.

Dependencies: A Cron Killer Feature

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.

Persistent Timers: Catching Up on Missed Runs

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.

Setting Up a systemd Timer from Scratch

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.

Step 1: Create the service unit

# /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

Step 2: Create the timer unit

# /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

Step 3: Enable and start the timer

# 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

When to Use Each

// decision guide
use cron
Quick personal scripts, one-liners, scripts that don't need monitoring, when you need to share configs across macOS/Linux/BSD, or when onboarding developers who don't know systemd.
use systemd
Production services, anything that needs structured logging, jobs with dependencies on other services (databases, networks), when missed-run recovery matters, or when security sandboxing is required.
use cron anyway
Legacy systems, containers without systemd (most Docker images don't run systemd โ€” use cron or a process supervisor like supervisord instead), non-Linux systems like macOS or FreeBSD.

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.

Verify your schedule first

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.