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
| Feature | Cron | Systemd Timer |
|---|---|---|
| Available since | 1979 (Unix V7) | 2012 (systemd 197) |
| Configuration | Single crontab line | Two unit files (.timer + .service) |
| Logging | Email or syslog | journalctl (structured) |
| Dependencies | None | After=, Requires=, network.target, etc. |
| Missed jobs (after reboot) | Lost (anacron for daily+) | Persistent=true catches up |
| Security sandboxing | None | Full systemd sandboxing |
| Resource limits | None | CPUQuota=, MemoryMax=, etc. |
| Randomized delay | Manual (sleep $RANDOM) | RandomizedDelaySec= |
| Learning curve | Low | Medium |
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 Expression | OnCalendar Equivalent | Description |
|---|---|---|
| * * * * * | *-*-* *:*:00 | Every minute |
| 0 * * * * | hourly | Every hour |
| 0 0 * * * | daily | Every day at midnight |
| 0 0 * * 0 | weekly | Every Sunday at midnight |
| 0 9 * * 1-5 | Mon..Fri *-*-* 09:00:00 | Weekdays at 9 AM |
| */15 * * * * | *-*-* *:0/15:00 | Every 15 minutes |
| 0 3 1 * * | *-*-01 03:00:00 | 1st 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:
- 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
- Create the service unit. Define what to run, as which user, with what restrictions.
sudo nano /etc/systemd/system/backup.service
- Create the timer unit. Convert the cron expression to OnCalendar format using the table above.
sudo nano /etc/systemd/system/backup.timer
- Test the service manually.
sudo systemctl daemon-reload sudo systemctl start backup.service sudo systemctl status backup.service journalctl -u backup.service --no-pager
- Enable the timer.
sudo systemctl enable --now backup.timer systemctl list-timers | grep backup
- 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 calendarto 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
Troubleshoot common cron failures on Linux.
Cron Job Monitoring Best PracticesMonitor scheduled tasks regardless of the scheduler.
Docker Cron Jobs GuideSchedule tasks in Docker where neither cron nor systemd is ideal.
Automate Database Backups with CronSet up automated backups with cron or systemd timers.
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.