You set up your first cron job and it worked. Then one day it didn't run, and you had no idea why. No error message, no log, nothing — cron just quietly skipped it while your machine was asleep.

That silence is cron's biggest weakness, and it's exactly what systemd timers fix. Timers are the modern scheduling system built into every major Linux distribution (Ubuntu, Debian, Fedora, Arch — all use systemd). They log every run, can catch up on missed jobs after a reboot, and use the same tooling as every other service on your system.

If you're new to the terminal, our Linux Terminal Basics guide will get you comfortable with the commands below.

How systemd Timers Work: Two Files, One Job

Every timer is made of two files, and the mental model is simple:

File Analogy Job
something.service The worker What to run
something.timer The alarm clock When to run it

The .timer file wakes up the .service file on schedule. Separate concerns, easy to reason about — and you can also run the service manually any time with systemctl start, which cron can't do.

Your First Timer in 3 Minutes

Let's schedule a script to run every day at 08:00. First, create the service file — this one just appends a line to a log file so we can see it working:

sudo nano /etc/systemd/system/hello.service
[Unit]
Description=Say hello every morning

[Service]
Type=oneshot
ExecStart=/bin/bash -c 'echo "Hello from systemd! Ran at: $(date)" >> /tmp/hello.log'

Note the /bin/bash -c '...' wrapper. systemd does not run ExecStart through a shell, so without it $(date) would print literally and the >> redirect would not create the file. Wrapping the whole command in /bin/bash -c '...' gives you shell features like variables and redirects.

Then the timer file:

sudo nano /etc/systemd/system/hello.timer
[Unit]
Description=Run hello.service daily at 8am

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

Now activate it:

sudo systemctl daemon-reload
sudo systemctl enable --now hello.timer

enable --now means "start at boot and start right now." Check it registered:

systemctl list-timers hello
# NEXT                        LEFT     LAST  PASSED  UNIT        ACTIVATES
# Fri 2026-08-15 08:00:00 CST  16h left  n/a   n/a     hello.timer hello.service

That's it. Tomorrow morning, check the log:

cat /tmp/hello.log
# Hello from systemd! Ran at: Fri Aug 15 08:00:01 CST 2026

OnCalendar: Cron Syntax, but Human

The schedule lives in the OnCalendar= line. It looks different from cron but follows the same idea:

You want Cron OnCalendar
Every day at 8 AM 0 8 * * * OnCalendar=*-*-* 08:00:00 or just daily
Every hour 0 * * * * hourly
Every 15 minutes */15 * * * * *:0/15
Mondays at 9 AM 0 9 * * 1 Mon *-*-* 09:00:00
First day of month 0 6 1 * * *-*-01 06:00:00
Every 5 minutes */5 * * * * *:0/5

The full pattern is DayOfWeek Year-Month-Day Hour:Minute:Second, and you can use names (daily, hourly, weekly, monthly) for the common cases. You can also stack multiple OnCalendar= lines in one timer for multiple schedules.

Why Timers Beat Cron (The Practical Bits)

  1. Everything is logged. Every run lands in journalctl -u hello.service — timestamps, output, errors. Cron's default: silence.
  2. Missed jobs can catch up. Persistent=true means: if your machine was off at 08:00, the job runs as soon as it boots. Cron just skips it forever.
  3. One tool for everything. systemctl status shows whether your timer is active, when it last ran, and when it runs next — the same commands you'd use for any service.
  4. Run X minutes after boot. OnBootSec=5min triggers a job 5 minutes after startup — great for things that shouldn't run while the system is still settling. Cron has a @reboot but nothing this precise.

Beyond the Calendar: Monotonic Timers

OnCalendar asks "what time is it?" Monotonic timers ask a different question: "how long since something happened?" — and for many jobs that's the better question.

Directive Meaning Use for
OnBootSec=5min 5 minutes after boot Jobs that wait for the network/disk to settle
OnUnitActiveSec=1h 1 hour after the timer was last activated Run every hour, but never overlapping
OnStartupSec=10s 10 seconds after systemd itself starts Very early boot tasks

The subtle win of OnUnitActiveSec: the next run is counted from when the previous run finished, not from a wall clock. A 10-minute job scheduled OnUnitActiveSec=1h runs at 09:00, 10:10, 11:20 — it never piles up on itself. That's something plain cron genuinely can't express.

You can mix both in one timer (e.g. OnCalendar=daily plus OnBootSec=5min), and systemd fires whichever comes first.

Real Project: Weekly Backups, the Reliable Way

Let's schedule the backup script from our Python Automated Backups guide — or any backup script you have — every Sunday at 03:00, with catch-up enabled:

sudo nano /etc/systemd/system/weekly-backup.service
[Unit]
Description=Run home directory backup

[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /home/user/backup.py
sudo nano /etc/systemd/system/weekly-backup.timer
[Unit]
Description=Weekly backup every Sunday at 3am

[Timer]
OnCalendar=Sun *-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now weekly-backup.timer

Now, the part cron never gave you — debugging. The day after, check what happened:

systemctl status weekly-backup.service
# ● weekly-backup.service - Run home directory backup
#      Loaded: loaded (/etc/systemd/system/weekly-backup.service; static)
#      Active: inactive (dead) since Sun 2026-08-16 03:00:04 CST

journalctl -u weekly-backup.service
# Aug 16 03:00:03 homeserver systemd[1]: Starting Run home directory backup...
# Aug 16 03:00:04 homeserver python3[2150]: Backup completed: 4.2 GB in 58s
# Aug 16 03:00:04 homeserver systemd[1]: weekly-backup.service: Deactivated successfully.

Exit code, output, duration — all there. When a backup fails, you'll actually know.

User-Level Timers (No sudo Needed)

Everything above is a system timer (requires sudo). For personal scripts, use user timers instead — same two files, different locations:

mkdir -p ~/.config/systemd/user
nano ~/.config/systemd/user/daily-report.timer   # your timer file here
systemctl --user daemon-reload
systemctl --user enable --now daily-report.timer

Add loginctl enable-linger yourusername once so user timers run even when you're not logged in.

When Cron Is Still the Right Choice

Fairness check: timers aren't always the answer.

  • Quick one-liners. crontab -e, one line, done — for a simple job you'll delete next week, cron is faster than two files and a daemon-reload.
  • Non-systemd systems. Alpine Linux, some containers, and BSDs don't use systemd — cron is your only option there.
  • Shared hosting. You often can't create system timers on cheap shared hosts, but crontab is usually available.

The rule of thumb: one-off or throwaway → cron. Anything that matters — backups, monitoring, scheduled reports — deserves a timer with its logs and catch-up behavior. Servers you care about tend to accumulate jobs of the second kind.

Test Your Timer Without Waiting for the Schedule

Nobody wants to discover a broken script at 3 AM. Two tricks let you verify everything right now:

Test the service immediately — the timer's job is just to trigger the service, so run the service by hand:

sudo systemctl start hello.service
cat /tmp/hello.log    # did it write?

If the service works when you run it manually, the timer will work too.

Verify your OnCalendar syntaxsystemd-analyze shows the next trigger times for any expression:

systemd-analyze calendar "Mon *-*-* 09:00:00"
#   Original form: Mon *-*-* 09:00:00
# Normalized form: Mon *-*-* 09:00:00
#     Next elapse: Mon 2026-08-17 09:00:00 CST
#        (in UTC): Mon 2026-08-17 01:00:00 UTC
#        From now: 2 days left

If you're unsure about a schedule string, this answers it in one command — no waiting for Monday to find out you typo'd it.

Common Pitfalls

  1. Forgot daemon-reload. After creating or editing a unit file, sudo systemctl daemon-reload is mandatory — without it, systemd reads a cached version and your changes silently don't apply.
  2. Wrong file permissions. System unit files must be root-owned (sudo nano handles that); user files in ~/.config/systemd/user/ must be owned by you.
  3. OnCalendar=*:0/5 vs *:*/5. For sub-hour schedules the syntax is *:0/5 (minute:second), not cron's */5. Easy to typo.
  4. Editing the service while the timer runs. If you change the .service, always daemon-reload — and note that enable --now only activates the timer; run sudo systemctl restart hello.timer if you edit the timer file itself.

What's Next?

Timers give your automation a real backbone — logs, recovery, and one consistent set of tools. Now apply it:

Migrate one cron job to a timer this week — pick the one that has failed silently before. You'll notice the difference the first time something goes wrong and you can actually see why.

All code in this article was tested and runs successfully on systemd 255 (Ubuntu, kernel 5.15.0) — verified August 2026. Both timers ran end-to-end at user level (identical syntax to the system-level units shown; system-level files need root): hello.service fired on schedule and appended to /tmp/hello.log, weekly-backup.service ran the backup script and wrote real output. This exposed and fixed a real bug — ExecStart does not go through a shell, so the original $(date) + redirect printed literally; the article now wraps the command in /bin/bash -c '...' (re-verified). All three OnCalendar expressions (daily, Sun -- 03:00:00, Mon -- 09:00:00) validated with systemd-analyze calendar; list-timers, status, and journalctl outputs matched.