Reliable Cron Jobs on a VPS: Use systemd Timers
Crontab silently fails and you never find out. Here is how to run reliable cron jobs on a VPS with systemd timers, logging, and failure alerts.
If you are running scheduled jobs on a VPS with plain crontab, you have a reliability problem you cannot see. Crontab runs your job, throws away the output, and tells you nothing when it fails. The correct way to run reliable cron jobs on a server is systemd timers: they log every run, survive reboots cleanly, do not overlap themselves, and let you alert on failure. I moved every scheduled job across my portfolio off crontab years ago and stopped losing jobs silently.
Why crontab fails you silently
Cron was designed in an era when a failed job emailed root, and almost nobody reads root's mailbox now. So when your nightly backup script errors out, or your billing job hits a database timeout, cron shrugs and moves on. You find out weeks later when a customer asks where their invoice went.
The other failure modes are worse because they are subtle. If a job takes longer than its interval, cron starts a second copy on top of the first. Now you have two backup processes fighting over the same file, or two workers double-charging the same customer. Cron has no idea the previous run is still going. It just fires again on schedule.
There is also no history. You cannot ask crontab "did the 3am job run last night, and how long did it take?" The information does not exist. For anything that matters, that is not good enough.
How systemd timers fix reliable scheduling
A systemd timer is two small files: a service unit that says what to run, and a timer unit that says when. The service runs your command, and systemd captures stdout and stderr into the journal automatically. Now every run is logged with a timestamp, exit code, and full output. You query it with journalctl -u yourjob.service and see exactly what happened.
The wins stack up fast:
- Overlap protection is free. A service is either active or not, so systemd will not start a second copy while the first is running.
Persistent=truemeans a job missed because the box was down runs on the next boot instead of vanishing.OnCalendar=syntax is readable.OnCalendar=*-*-* 03:00:00is clearer than a row of cron asterisks, and it handles time zones sanely.- You can add
OnFailure=to trigger an alert unit the moment a job exits non-zero.
That last point is the whole game. You want to know within minutes when a scheduled job fails, not when a customer complains. Wire the failure handler to your own alerting, which pairs well with self-hosting monitoring instead of paying Datadog.
How do I alert when a cron job fails?
Add an OnFailure=notify@%n.service line to your service unit and define a small notify@ template that posts to a webhook or sends an email. Now a failed job pushes a message to you with the unit name attached. No polling, no dashboard to watch. The job tells you when it breaks.
For jobs that must run, add a dead-man's-switch: a lightweight check-in to a service that alerts you if the expected ping does not arrive. This catches the case where the timer itself stopped firing, which a failure handler alone would miss. Between the two, you cover both "it ran and broke" and "it never ran." This is the same defensive posture I apply to background workers on a VPS: assume silent failure is the default and design against it.
Keep the jobs idempotent and logged
Reliable scheduling is not only about the runner. The job itself has to be safe to run twice. If a run gets retried after a partial failure, it should not double-process. Write jobs so re-running them converges to the same state instead of stacking side effects. That single discipline removes most of the damage a duplicate run could do.
Log what the job did, not just that it ran. A backup job should log how many bytes it wrote and where. A billing job should log how many invoices it generated. When something looks off weeks later, that log is the difference between a five-minute answer and a forensic investigation.
None of this needs a managed scheduler or a cloud cron service. It is built into every modern Linux box, and running it yourself on HostSSH means the scheduler lives right next to the work it triggers, with no extra service to pay for or depend on. If you already run a fleet, fold the timer definitions into your standard setup so every box gets them, the way I describe in one runbook for twenty apps. Crontab was fine for 1985. Use the tool that actually tells you the truth.