Goal: choose the right scheduler for recurring jobs on a Linux VPS and migrate your crontab to systemd timers where it pays off. This guide compares both, shows a working timer unit, and covers the pitfalls (missed runs, timezone handling, resource limits). Prerequisites: root access and a few cron jobs you are willing to experiment on.
When cron is still the right tool
Cron remains the simplest option for basic jobs: one line in a crontab, no unit files, no daemon-reload. It is fine for log rotation triggers, simple cleanup scripts, and anything that must run at an exact wall-clock minute. It falls short when you need dependencies, missed-run catch-up, sandboxing, or per-service resource limits — all of which systemd timers provide natively.
Anatomy of a systemd timer
A timer needs two unit files. The service holds the command; the timer schedules it:
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly offsite backup
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/backup.sh
User=backup
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup nightly at 02:30
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
Enable both:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer
The Persistent=true line is the killer feature: if the machine was off at 02:30, the job runs immediately at next boot. Cron simply drops missed runs.
Calendar vs monotonic scheduling
| Timer type | Directive | Use case |
|---|---|---|
| Wall-clock | OnCalendar=*-*-* 02:30:00 | Nightly backups, log rotation, TLS renewal |
| Daily offset | OnCalendar=daily | Simple daily jobs at midnight |
| Monotonic | OnUnitActiveSec=6h | Jobs that must run every N hours since last run |
| Monotonic | OnBootSec=5min | One-off startup tasks after boot |
Use monotonic timers for jobs whose spacing matters (health checks, cache warmers) and calendar timers for jobs that must align with wall-clock time. You can combine both in one timer; the job runs when either condition fires.
Missed runs, timezones, and randomization
Set the timer’s timezone explicitly or inherit the system one; a VPS with UTC while your users are in UTC+2 will fire at the wrong local hour. Add RandomizedDelaySec=10m to spread load, and use AccuracySec=1m when you need precise firing. Check the next run with:
systemctl list-timers --all
systemd-analyze calendar backup.timer
OnCalendar syntax examples
The calendar syntax is more flexible than cron’s five fields. Common patterns:
| Intention | cron equivalent | OnCalendar value |
|---|---|---|
| Every day at 02:30 | 30 2 * * * | *-*-* 02:30:00 |
| Every Monday 03:00 | 0 3 * * 1 | Mon *-*-* 03:00:00 |
| First day of month 04:00 | 0 4 1 * * | *-*-01 04:00:00 |
| Every 15 min | */15 * * * * | *:0/15 |
| Twice a day | 0 */12 * * * | *-*-* 00,12:00:00 |
Validate any expression before deploying it — a typo silently never fires. systemd-analyze calendar prints the next 10 occurrences so you can eyeball them.
Cron pitfalls timers eliminate
- Missed runs: cron drops jobs when the machine is down; Persistent=true catches up
- No logging: cron output goes to mail or /dev/null; timers log to the journal per unit
- No dependency tracking: timer jobs race with boot; timers support After=/Requires=
- No resource control: a runaway cron job can OOM your web server; timers get MemoryMax/CPUQuota
- Timezone handling: cron uses the system TZ without per-job overrides
That last point matters on VPS fleets: if you manage servers in different timezones, systemd timers let you encode the intended schedule unambiguously in the unit file instead of relying on each host’s local time.
Hardening timers
Because systemd runs the job as a unit, you get sandboxing and limits for free:
[Service]
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
MemoryMax=256M
CPUQuota=50%
This caps a runaway backup script at 256 MB and half a CPU core instead of letting it starve your web server — something cron cannot do without external wrappers like timeout and ulimit.
Migration checklist
- Convert one cron line at a time; keep cron running for the rest
- Use Persistent=true so no run is lost during the transition
- Test with systemctl start backup.service (fires immediately)
- Check logs: journalctl -u backup.service
- Remove the cron line only after two successful timer runs
Example conversion: 0 3 * * * certbot renew becomes an OnCalendar=*-*-* 03:00:00 timer on a oneshot service running certbot renew --quiet. The journal gives you structured logs per job, and systemctl list-timers shows the next run for every scheduled task.
Timers are the better default for anything on a modern VPS with systemd (Ubuntu 16.04+, Debian 8+, RHEL 7+). Keep cron only for legacy scripts or where a single crontab line is genuinely simpler. For more scheduling-adjacent tuning, virtualserversvps.com covers logrotate and journald configuration so scheduled jobs do not fill the disk.
When you are ready to move the workload to a provider that makes provisioning fast, InterServer VPS plans with full root access is a cheap place to test timers, systemd hardening, and your backup scripts. Before you commit, compare VPS providers on our comparison table to check which hosts offer the systemd and kernel versions you need.




Leave a Reply
You must be logged in to post a comment.