Using systemd Timer Instead of Cron on Linux VPS
Most Linux administrators learned cron first — it is simple, reliable, and universally available. However, systemd timers offer significant advantages on modern VPS instances that already run systemd: integrated logging through journald, dependency ordering, and the ability to catch up on missed runs after reboots.
cron vs systemd timer: Key Differences
| Feature | cron | systemd timer |
|---|---|---|
| Logging | Must redirect stdout manually | journalctl shows all output |
| Catch-up after reboot | Not supported | Supported via Persistent=true |
| Dependencies | None | After=network.target, etc. |
| Relative timing | Not supported | OnBootSec, OnUnitActiveSec |
| Temporary disable | Must edit crontab | systemctl disable --now |
Components of a systemd Timer
Each timer consists of two files placed in /etc/systemd/system/:
- Service unit (
myapp.service) — defines the command to run - Timer unit (
myapp.timer) — defines when to trigger the service
Creating Your First systemd Timer
Example: run a backup script every day at 02:00.
1. Create the service unit
sudo nano /etc/systemd/system/daily-backup.service
[Unit]
Description=Daily backup script
After=network.target
[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/backup.sh
StandardOutput=journal
StandardError=journal
Type=oneshot is correct for tasks that run once and exit — equivalent to how cron works. Use simple only for long-running daemons.2. Create the timer unit
sudo nano /etc/systemd/system/daily-backup.timer
[Unit]
Description=Run daily-backup.service every day at 02:00
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
Persistent=true ensures that if the server was off at the scheduled time, the job runs immediately after boot — preventing silent backup failures during maintenance windows.3. Enable and start the timer
# Reload systemd to pick up new unit files
sudo systemctl daemon-reload
# Enable timer to start on boot and start it now
sudo systemctl enable --now daily-backup.timer
# Check timer status
sudo systemctl status daily-backup.timer
Common OnCalendar Expressions
| Schedule | OnCalendar value |
|---|---|
| Every hour | hourly |
| Daily at 03:00 | *-*-* 03:00:00 |
| Every Monday at 09:00 | Mon *-*-* 09:00:00 |
| First of month | *-*-01 00:00:00 |
| Every 5 minutes | *:0/5 |
| Every 30 seconds | OnUnitActiveSec=30s |
Boot-relative Timers (OnBootSec)
Use OnBootSec when you want timing relative to system boot rather than wall-clock time:
[Timer]
OnBootSec=5min
OnUnitActiveSec=1h
This runs the service 5 minutes after boot, then repeats every hour thereafter.
User-level Timers (No sudo Required)
Timers can run under a regular user account without root privileges:
# Create user systemd directory
mkdir -p ~/.config/systemd/user/
# Create service
nano ~/.config/systemd/user/myapp-task.service
[Unit]
Description=My app periodic task
[Service]
Type=oneshot
ExecStart=/home/user/scripts/run-task.sh
nano ~/.config/systemd/user/myapp-task.timer
[Unit]
Description=Run myapp-task every 6 hours
[Timer]
OnCalendar=*-*-* 00/6:00:00
Persistent=true
[Install]
WantedBy=timers.target
# Enable without sudo
systemctl --user daemon-reload
systemctl --user enable --now myapp-task.timer
systemctl --user list-timers
sudo loginctl enable-linger usernameListing All Timers
# List all system timers
systemctl list-timers --all
# List only active timers
systemctl list-timers
# List user-level timers
systemctl --user list-timers
Verify That systemd Timer Is Working Correctly
# Check timer status
systemctl status daily-backup.timer
# Check when service last ran successfully
systemctl status daily-backup.service
# View service logs
journalctl -u daily-backup.service -n 50
# View logs from the past 24 hours
journalctl -u daily-backup.service --since "24 hours ago"
# Manually trigger the service to test it immediately
systemctl start daily-backup.service
systemd-analyze calendar "*-*-* 02:00:00" to validate your calendar expression and see the next scheduled trigger time.Troubleshooting Common Errors
Error: "Failed to enable unit: Unit file daily-backup.timer does not exist"
systemd has not loaded the new unit file, or the filename is incorrect.
# Always reload after creating unit files
sudo systemctl daemon-reload
# Confirm the file exists with correct name
ls -la /etc/systemd/system/daily-backup.*
# Validate unit file syntax
systemd-analyze verify /etc/systemd/system/daily-backup.timer
Error: "daily-backup.service: Main process exited, code=exited, status=127"
Exit code 127 means the command was not found — the script path is wrong or the file lacks execute permission.
# Verify script exists and is executable
ls -la /usr/local/bin/backup.sh
chmod +x /usr/local/bin/backup.sh
# ExecStart must use an absolute path
# Wrong: ExecStart=backup.sh
# Correct: ExecStart=/usr/local/bin/backup.sh
Error: Timer does not run at scheduled time
Check that the timer is active and the system clock matches the expected timezone.
# Confirm timer is active
systemctl is-active daily-backup.timer
# Check system time and timezone
timedatectl status
# Show next trigger time
systemctl list-timers daily-backup.timer
Error: "daily-backup.service: Skipped due to already running"
A previous run is still in progress when the timer fires again.
# Ensure Type=oneshot and RemainAfterExit=no
[Service]
Type=oneshot
RemainAfterExit=no
# Or reset a failed/stale service state
systemctl reset-failed daily-backup.service
Error: "Condition check resulted in daily-backup.service being skipped"
A ConditionPathExists or similar condition directive is not satisfied.
# Review conditions defined in the service
systemctl cat daily-backup.service
# Check logs for which condition failed
journalctl -u daily-backup.service -n 20 --no-pager
Frequently Asked Questions
Should I switch from cron to systemd timers on my VPS?
On modern Ubuntu, Debian, or CentOS VPS instances, systemd timers are generally the better choice: logs are automatically captured in journald without redirection, and Persistent=true prevents missed runs during reboots. Stick with cron if you need portability across systems without systemd or prefer the concise syntax for simple schedules.
How do I view the output of a job run through a systemd timer?
Use journalctl -u servicename.service — no log file configuration required. Add -f for live output, or --since "1 hour ago" to filter by time range. The service and its timer share the same unit name for easy filtering.
Can a systemd timer run multiple commands?
With Type=oneshot, you can add multiple ExecStart= lines in a single service unit and they execute sequentially. Alternatively, point ExecStart at a shell script that contains all the commands — this keeps the unit file clean and makes all output visible in a single journal stream.