systemd Timers vs cron: Scheduling Jobs on a Linux Server
cron is simple and everywhere; systemd timers give you logging, missed-run catch-up, randomized delays, resource limits and dependencies. A side-by-side comparison, a complete timer and service example with OnCalendar, how to test calendar expressions, list and debug timers, and which to choose.
Every server needs scheduled jobs: nightly backups, cleanup scripts, sending a daily digest. On Linux you have two built-in options: classic cron and systemd timers. (Background Jobs and Cron)
Side by side
| cron | systemd timer | |
|---|---|---|
| Setup | One line in a crontab | Two small files (.timer + .service) |
| Logs | Mailed or lost unless you redirect them | In the journal automatically (journalctl) |
| Missed runs (server was off) | Skipped | Can catch up with Persistent=true |
| Overlapping runs | Possible | No — a running service isn't started twice |
| Random delay (avoid thundering herd) | Manual sleep |
RandomizedDelaySec= |
| Resource limits, sandboxing | No | Yes — all systemd service options |
| Dependencies ("after the network is up") | No | Yes |
| Run it manually, exactly as scheduled | Copy the command | systemctl start job.service |
cron wins on simplicity. Timers win on everything you'll want once a job matters.
A cron job
crontab -e
17 3 * * * /home/app/bin/backup.sh >> /var/log/backup.log 2>&1
Remember cron runs with a minimal environment — not your login shell's PATH or variables — which is the cause of most "works in my terminal, fails in cron" problems. (Cron Expressions Explained, PATH and Command Not Found)
The same job as a systemd timer
The service — what to run:
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly database backup
[Service]
Type=oneshot
User=app
ExecStart=/home/app/bin/backup.sh
# Optional hardening and limits
Nice=10
MemoryMax=512M
The timer — when to run it:
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup nightly
[Timer]
OnCalendar=*-*-* 03:17:00
Persistent=true
RandomizedDelaySec=5min
[Install]
WantedBy=timers.target
Enable and start the timer (not the service):
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
Persistent=true— if the server was off at 03:17, run as soon as it boots.RandomizedDelaySec— spreads start times so many servers don't hit an API or backup target at the same second.Type=oneshot— the service runs to completion, then stops.
(Writing a systemd Service File, What Is systemd?)
OnCalendar examples
| Expression | Meaning |
|---|---|
hourly |
Every hour at :00 |
daily |
Every day at midnight |
Mon *-*-* 09:00 |
Mondays at 09:00 |
Mon..Fri 08:30 |
Weekdays at 08:30 |
*:0/15 |
Every 15 minutes |
*-*-01 04:00 |
1st of each month at 04:00 |
Test an expression before using it:
systemd-analyze calendar "Mon..Fri 08:30"
It prints the normalised form and the next time it will fire. Timers can also run relative to events: OnBootSec=5min, OnUnitActiveSec=1h.
Seeing what's scheduled and what happened
systemctl list-timers # every timer, last run and next run
systemctl status backup.service # last result
journalctl -u backup.service -n 50 # output from recent runs
sudo systemctl start backup.service # run it right now, the same way the timer does
That last command is the killer feature: you test the job in exactly the environment it will run in.
Getting alerted on failure
Add OnFailure= to the service's [Unit] section to trigger another unit (for example, one that sends an email or Slack message) whenever the job fails. Or have your script ping a heartbeat monitor on success, so silence means failure. (Know When Your App Is Down)
Which should you use?
- Quick personal job, or a system without systemd (some containers): cron.
- Anything production — backups, billing, data syncs: a systemd timer, for the logs, catch-up and no overlaps.
- Jobs that belong to your app (send this email in 10 minutes, retry on failure): a job queue inside the app. (Postgres SKIP LOCKED Job Queue, BullMQ Tutorial)
The summary
- cron: one line, minimal features, silent failures.
- systemd timers: a
.timer+.servicepair with logging,Persistent=true, random delays and limits. - Use
systemctl list-timers,journalctl -u, andsystemd-analyze calendarto manage them.
EasySpawn gives each app a persistent server where scheduled jobs run next to your database, with daily backups already handled — and Claude Code to write the timers. See how it works or join the waitlist.
Related: Bash Scripting for Beginners · PM2 vs systemd · Run a Python Script 24/7 · Scheduled GitHub Actions
Keep reading
SSH Port Forwarding Explained: Local, Remote, and Dynamic Tunnels
SSH tunnels let you reach a database or web app on a server as if it were on localhost — without opening ports to the internet. Local (-L), remote (-R) and dynamic (-D) forwarding with practical examples, background tunnels, and the security caveats.
The SSH Config File Explained: Stop Typing Long SSH Commands
~/.ssh/config turns ssh -i ~/.ssh/key -p 2222 deploy@203.0.113.10 into ssh prod. Where the file lives on each OS, the options worth knowing, multiple GitHub accounts, jump hosts, keep-alives, and the precedence rule that trips people up.