Using systemd timers instead of cron jobs
What this is
Every modern distro ships a second scheduler alongside cron: systemd timers. For quick one-liners, cron is fine. For jobs that matter, backups, certificate renewals, database dumps, timers fix every one of cron's classic failure modes (the ones that fill the cron troubleshooting guide), and they're only slightly more work to set up.
What you get over cron
- Every run is logged.
journalctl -u myjob.serviceshows each execution with its full output and exit status, no more "did it even run?" archaeology, no MAILTO plumbing. - No overlapping runs, by design. A timer won't start the service while the previous run is still going, the cron-pileup fork bomb can't happen, no
flockneeded. - Missed runs can catch up. With
Persistent=true, a job whose moment passed while the VPS was off or rebooting runs at the next boot, cron just skips it silently. (For a VPS that gets suspended and reactivated, that difference is your nightly dump actually happening.) - No cron environment quirks. No
%escaping, saner behavior, and anything the job needs can be declared explicitly in the unit. - Resource caps.
CPUQuota=50%orMemoryMax=1Gon the service keeps a heavy backup or reindex job from starving the site it's supposed to protect. - Ordering.
After=network-online.targetand friends, so a job that needs the network doesn't fire before it exists.
The recipe: two small files
A scheduled job is a service (what to run) plus a timer (when). Say it's a nightly backup script:
/etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
/etc/systemd/system/backup.timer
[Unit]
Description=Run nightly backup at 03:30
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
RandomizedDelaySec=10m
[Install]
WantedBy=timers.target
Then activate:
systemctl daemon-reload
systemctl enable --now backup.timer
(RandomizedDelaySec spreads the start over a window, a good habit so all your jobs don't stampede at the same second.)
Operating it
- See all timers, with last and next run:
systemctl list-timers - Read a job's history and output:
journalctl -u backup.service - Run it right now (without waiting for the schedule):
systemctl start backup.service - Schedule syntax:
OnCalendartakes shorthands (daily,hourly,weekly) or full expressions (Mon *-*-* 06:00:00). Verify any expression before trusting it, systemd has its own crontab.guru built in:
systemd-analyze calendar "Mon *-*-* 06:00:00"
which prints exactly when it will fire next.
The gotchas
- Enable the
.timer, not the.service. Enabling the service makes it run at boot; the timer is what schedules it. (enable --now backup.timeris the whole incantation.) Type=oneshotis right for scripts that run and exit, which is what scheduled jobs are.- Jobs that shouldn't run as root: add
User=deployto the[Service]section.
When cron is still fine
A one-line log cleanup, a quick curl ping, anything where a silent miss costs nothing, crontab is faster to type and perfectly adequate (its failure modes and fixes). The rule of thumb: if you'd be upset to learn the job hadn't run for a month, it belongs in a timer.
Still need help?
You can open a support ticket. So we can help on the first reply, it's worth mentioning:
- the VPS hostname or IP,
- the .timer and .service files,
- what
systemctl list-timersand the job's journalctl output show.
Related questions
- "Should I use a systemd timer or a cron job?"
- "How do I create a systemd timer?"
- "How do I see when my timer last ran and what it printed?"
- "What does Persistent=true do?"
- "How do I verify an OnCalendar expression?"
- "How do I stop a scheduled job from overlapping itself?"