Back to Blog
Linux13 min read

Cron vs Systemd Timers: Which Should You Use?

For decades, cron was the only game in town for scheduling tasks on Linux. Then systemd timers arrived with better logging, dependency management, and security features. Both are capable tools, but they solve the problem differently. This guide breaks down when to use each one, and when to use neither.

Feature Comparison at a Glance

FeatureCronSystemd Timer
Available since1979 (Unix V7)2012 (systemd 197)
ConfigurationSingle crontab lineTwo unit files (.timer + .service)
LoggingEmail or syslogjournalctl (structured)
DependenciesNoneAfter=, Requires=, network.target, etc.
Missed jobs (after reboot)Lost (anacron for daily+)Persistent=true catches up
Security sandboxingNoneFull systemd sandboxing
Resource limitsNoneCPUQuota=, MemoryMax=, etc.
Randomized delayManual (sleep $RANDOM)RandomizedDelaySec=
Learning curveLowMedium

Cron: The Classic Approach

You have probably seen this format hundreds of times. A cron expression is five fields followed by the command:

# Edit your crontab
crontab -e

# Run a backup every day at 3:00 AM
0 3 * * * /opt/scripts/backup.sh

# Health check every 5 minutes
*/5 * * * * curl -s https://myapp.com/health > /dev/null

# Weekly cleanup on Sunday at 2:00 AM
0 2 * * 0 /usr/bin/find /tmp -name "*.tmp" -mtime +7 -delete

Strengths: one line per job, universally understood, works on every Unix-like system including macOS and BSD, zero boilerplate.

Weaknesses: output goes to email (which nobody reads), no built-in logging, no retry logic, no dependency management, and if the server is down when a job should run, the job is simply missed.

Systemd Timers: The Modern Alternative

A systemd timer requires two files: a .timer unit that defines the schedule, and a .service unit that defines what to execute. Here is the same daily backup:

# /etc/systemd/system/backup.service
[Unit]
Description=Daily database backup
After=network.target mysql.service

[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
User=backup
Group=backup

# Security sandboxing
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/backups
PrivateTmp=true
NoNewPrivileges=true

# Resource limits
MemoryMax=512M
CPUQuota=50%
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 3:00 AM

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target
# Enable and start the timer
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer

# Check timer status
systemctl list-timers --all | grep backup
# NEXT                        LEFT     LAST                        PASSED   UNIT
# Tue 2026-03-12 03:00:00 UTC 12h left Mon 2026-03-11 03:02:17 UTC 11h ago  backup.timer

The OnCalendar Syntax

Systemd uses its own calendar expression format, which is more verbose than cron but arguably more readable:

Cron ExpressionOnCalendar EquivalentDescription
* * * * **-*-* *:*:00Every minute
0 * * * *hourlyEvery hour
0 0 * * *dailyEvery day at midnight
0 0 * * 0weeklyEvery Sunday at midnight
0 9 * * 1-5Mon..Fri *-*-* 09:00:00Weekdays at 9 AM
*/15 * * * **-*-* *:0/15:00Every 15 minutes
0 3 1 * **-*-01 03:00:001st of month at 3 AM

You can validate OnCalendar expressions without deploying:

# Preview next trigger times
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"
#   Original form: Mon..Fri *-*-* 09:00:00
#   Next elapse: Mon 2026-03-16 09:00:00 UTC
#        (in UTC): Mon 2026-03-16 09:00:00 UTC
#        From now: 4 days left

systemd-analyze calendar "*-*-* *:0/15:00"
#   Next elapse: Tue 2026-03-11 15:00:00 UTC

Logging: Where Systemd Timers Shine

This is the area where systemd timers have the biggest practical advantage over cron. With cron, output goes to the local mail spool by default. Most servers do not have a mail transfer agent configured, so that output vanishes into the void. You end up redirecting to a file manually:

# Cron: manual log redirection
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

With systemd, all output from the service is automatically captured by the journal. You get structured logs with timestamps, log levels, and the ability to filter by unit, time range, or priority:

# View logs for the backup service
journalctl -u backup.service

# Last 10 entries
journalctl -u backup.service -n 10

# Logs from today only
journalctl -u backup.service --since today

# Logs between specific times
journalctl -u backup.service --since "2026-03-10" --until "2026-03-11"

# Follow live output
journalctl -u backup.service -f

# Show only errors
journalctl -u backup.service -p err

You can also check whether the last run succeeded or failed:

systemctl status backup.service
# Shows: Active: inactive (dead) since ... trigger: backup.timer
# Shows: Main PID: 12345 (code=exited, status=0/SUCCESS)

Persistent Timers: Catching Up After Downtime

One of the most impactful features of systemd timers is Persistent=true. When enabled, systemd records the last time the timer fired. If the system is off or rebooting when a timer should fire, systemd runs the job immediately on the next boot.

Cron has no equivalent built-in. If a server is down during a scheduled cron job, that execution is lost. The workaround is anacron, but it only works for daily, weekly, and monthly schedules, not for sub-hourly jobs.

Security Sandboxing

Cron jobs run with the full permissions of the user who owns the crontab. If a backup script has a vulnerability, an attacker could potentially read or modify anything that user has access to.

Systemd services support granular sandboxing. Here is a realistic security configuration for a backup task:

[Service]
# Run as a dedicated user
User=backup
Group=backup

# Filesystem restrictions
ProtectSystem=strict          # Mount / as read-only
ProtectHome=true              # Hide /home, /root, /run/user
ReadWritePaths=/var/backups   # Only allow writes here
PrivateTmp=true               # Isolated /tmp

# Privilege restrictions
NoNewPrivileges=true          # Cannot gain new capabilities
CapabilityBoundingSet=        # Drop all capabilities
RestrictSUIDSGID=true         # Block SUID/SGID files

# Network restrictions
RestrictAddressFamilies=AF_INET AF_INET6  # Only IPv4/IPv6
# Or for jobs that need no network:
# PrivateNetwork=true

# System call filtering
SystemCallFilter=@system-service
SystemCallArchitectures=native

This level of isolation is simply not possible with cron. If security is a priority for your scheduled tasks, systemd timers are the clear choice on any system that runs systemd.

Migration Guide: Cron to Systemd Timer

Follow these steps to migrate an existing cron job:

  1. Identify the cron job. Note the schedule, command, user, and any environment variables.
    # Original crontab entry:
    MAILTO=ops@company.com
    0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
  2. Create the service unit. Define what to run, as which user, with what restrictions.
    sudo nano /etc/systemd/system/backup.service
  3. Create the timer unit. Convert the cron expression to OnCalendar format using the table above.
    sudo nano /etc/systemd/system/backup.timer
  4. Test the service manually.
    sudo systemctl daemon-reload
    sudo systemctl start backup.service
    sudo systemctl status backup.service
    journalctl -u backup.service --no-pager
  5. Enable the timer.
    sudo systemctl enable --now backup.timer
    systemctl list-timers | grep backup
  6. Remove the cron entry. Only after verifying the timer works.
    crontab -e
    # Comment out or delete the old entry

When to Use Each

Use cron when...

  • - You need quick, simple scheduling with no ceremony
  • - You are on macOS, BSD, or a system without systemd
  • - The task is a one-liner and you value brevity
  • - You are on shared hosting where you only have crontab access

Use systemd timers when...

  • - You need structured logging and easy debugging
  • - You want missed jobs to run after a reboot (Persistent=true)
  • - The task needs security sandboxing or resource limits
  • - Your task depends on other services (network, database)
  • - You want randomized delays to avoid thundering herd effects

Use neither when...

  • - You are on serverless, PaaS, or containers without system-level access
  • - You need failure alerts, automatic retries, and execution dashboards
  • - You need timezone-aware scheduling with proper DST handling
  • - Multiple team members need to manage schedules without SSH access
  • - Your tasks are HTTP endpoints on a web application

For that last category, an external HTTP scheduler like CronJobPro fills the gap. It combines the simplicity of cron's schedule syntax with the monitoring and alerting features that neither cron nor systemd timers provide out of the box.

Key Takeaways

  • Cron is simpler (one line vs two files) and universally available. It is hard to beat for quick, straightforward scheduling.
  • Systemd timers provide structured logging via journalctl, persistent timers that survive reboots, and security sandboxing that cron cannot match.
  • Use systemd-analyze calendar to validate OnCalendar expressions before deploying.
  • Neither cron nor systemd timers provide monitoring dashboards, failure alerts, or automatic retries. For those features, use an external scheduler.
  • Migration is straightforward: convert the expression, create two unit files, test, enable, and remove the old cron entry.

Related Articles

Beyond cron and systemd timers

CronJobPro gives you monitoring dashboards, automatic retries, failure alerts, and timezone-aware scheduling for your HTTP endpoints. No server access needed.