Why systemd Timers Are the New Cron (and How to Use Them)
If you’ve administered a Linux server for more than five minutes, you’ve probably edited a crontab. It’s one of those rituals — like configuring your .bashrc or setting up SSH keys. Cron has been around since 1975, and it works. But in 2026, there’s a better way: systemd timers.
systemd timers are what cron would look like if it were designed today. They offer logging, dependency management, retry logic, and calendar scheduling that actually makes sense. And they’re already on your system — every major Linux distribution ships with systemd.
In this tutorial, we’ll replace a typical cron job with a systemd timer, step by step. By the end, you’ll wonder why you didn’t switch sooner.
Prerequisites
- A Linux system with systemd (Ubuntu 16.04+, Debian 9+, CentOS 7+, Fedora, Arch, etc.)
- Basic familiarity with the command line
- A script you want to run on a schedule (we’ll create one)
sudoaccess
Step 1: Create Your Script
Let’s say you want to back up a directory every day at 2 AM. First, create the script:
sudo mkdir -p /opt/scripts
sudo nano /opt/scripts/daily-backup.sh
Add this content:
#!/bin/bash
# Daily backup script
BACKUP_SRC="/home/noodly/projects"
BACKUP_DEST="/backups/projects"
DATE=$(date +%Y-%m-%d)
LOG="/var/log/daily-backup.log"
echo "[$(date)] Starting backup..." >> "$LOG"
mkdir -p "$BACKUP_DEST"
tar czf "$BACKUP_DEST/backup-$DATE.tar.gz" "$BACKUP_SRC" 2>> "$LOG"
if [ $? -eq 0 ]; then
echo "[$(date)] Backup completed successfully." >> "$LOG"
else
echo "[$(date)] Backup FAILED!" >> "$LOG"
fi
# Keep only the last 7 backups
ls -t "$BACKUP_DEST"/backup-*.tar.gz | tail -n +8 | xargs rm -f
Make it executable:
sudo chmod +x /opt/scripts/daily-backup.sh
Step 2: Create the systemd Service Unit
A systemd timer needs a service unit to tell it what to run. Think of the service as the “what” and the timer as the “when.”
sudo nano /etc/systemd/system/daily-backup.service
Add this content:
[Unit]
Description=Daily backup of projects directory
After=network.target
[Service]
Type=oneshot
ExecStart=/opt/scripts/daily-backup.sh
User=noodly
Group=noodly
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/backups/projects /var/log
Let’s break down what each part does:
Type=oneshot— This tells systemd the script runs once and exits. Use this for backup scripts, cleanup tasks, and anything that isn’t a long-running daemon.ExecStart=— The command to run. Same thing you’d put in crontab.User/Group— Run as a specific user instead of root. This is cleaner than cron’s per-user crontabs.- The security hardening lines (
NoNewPrivileges,ProtectSystem, etc.) — These are bonus features cron can’t give you. They sandbox your script so even if it’s compromised, the damage is limited.
Step 3: Create the systemd Timer Unit
Now the fun part — the timer that controls when the service runs:
sudo nano /etc/systemd/system/daily-backup.timer
Add this content:
[Unit]
Description=Run daily backup at 2:00 AM
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
Here’s what makes this powerful:
OnCalendar=— This is cron’s schedule syntax, but readable.*-*-* 02:00:00means “every day at 2:00 AM.” You can also doMon,Fri 09:00:00for specific days, or*:0/30for every 30 minutes.Persistent=true— If the server was off at 2 AM, the backup runs immediately when it boots. Cron would just skip it.RandomizedDelaySec=300— Adds a random delay of up to 5 minutes. This is great if you have multiple timers that would otherwise all fire at exactly the same instant and spike your CPU.
Step 4: Enable and Start the Timer
Reload systemd to pick up the new units, then enable and start the timer:
sudo systemctl daemon-reload
sudo systemctl enable --now daily-backup.timer
That’s it. The --now flag starts the timer immediately, so you don’t have to wait until 2 AM to verify it works.
Step 5: Verify Everything Is Working
Check the timer status:
systemctl status daily-backup.timer
You should see something like:
● daily-backup.timer - Run daily backup at 2:00 AM
Loaded: loaded (/etc/systemd/system/daily-backup.timer; enabled; preset: enabled)
Active: active (waiting) since Sat 2026-06-21 12:00:00 UTC
Trigger: Sun 2026-06-22 02:00:00 UTC; 14h left
List all active timers to see when they’re scheduled to fire:
systemctl list-timers
This gives you a beautiful overview of every timer on the system — something cron simply cannot do.
Check the logs for your service:
journalctl -u daily-backup.service
This is one of the biggest advantages over cron. Every run is logged automatically. No more redirecting output to a file and hoping for the best.
Common OnCalendar Patterns
Here’s a quick reference for schedule expressions:
| Pattern | Meaning |
|---|---|
*-*-* 02:00:00 |
Every day at 2 AM |
Mon,Fri 09:00:00 |
Monday and Friday at 9 AM |
Mon..Fri *-*-* 08:00:00 |
Weekdays at 8 AM |
*:0/30:00 |
Every 30 minutes |
*-*-01 00:00:00 |
First day of every month at midnight |
Sat *-*-* 03:00:00 |
Every Saturday at 3 AM |
Common Pitfalls and Troubleshooting
1. Forgetting daemon-reload
After editing any .service or .timer file, you must run sudo systemctl daemon-reload. systemd won’t pick up your changes until you do. This trips up everyone at least once.
2. Timer shows “n/a” for next trigger
If systemctl list-timers shows n/a for the next trigger, your OnCalendar syntax is probably wrong. Test it with:
systemd-analyze calendar "*-*-* 02:00:00"
This will parse your expression and show the next few trigger times.
3. Service runs but produces no output
By default, systemd captures all stdout/stderr. Use journalctl -u your-service.service to see it. If you’re debugging, add StandardOutput=journal to the [Service] section.
4. Timer doesn’t fire after reboot
Make sure you used Persistent=true. Without it, if the system was off when the timer was supposed to fire, that execution is simply lost. With Persistent=true, systemd catches up on the next boot.
5. Permission denied errors
Remember that the service runs as the User= you specified. Make sure that user has permission to read the source files and write to the destination. Test by running the script manually as that user:
sudo -u noodly /opt/scripts/daily-backup.sh
Bonus: Adding Retry Logic
Here’s something cron absolutely cannot do natively — automatic retries. Add these lines to your [Service] section:
[Service]
...
Restart=on-failure
RestartSec=60
Now if your backup script fails (maybe a network mount was temporarily unavailable), systemd will automatically retry after 60 seconds. You can also add StartLimitIntervalSec and StartLimitBurst to cap the number of retries.
Wrapping Up
systemd timers give you everything cron does, plus:
- Built-in logging via journald — no more
2>>&1 >> /var/log/whatever.log - Persistent timers that catch up after downtime
- Security sandboxing with
ProtectSystem,NoNewPrivileges, and friends - Dependency management — your timer can wait for network, mounts, or other services
- Retry logic with automatic restart on failure
- Centralized management —
systemctl list-timersshows everything at a glance
Cron isn’t going anywhere — it’s simple, universal, and perfect for quick one-liners. But for anything you care about running reliably on a systemd-based Linux system, timers are the modern choice.
Your future self — the one reading clean logs at 3 AM instead of debugging why a cron job silently failed — will thank you.
Leave a Reply