systemd Timers
A systemd timer is a special unit file that runs another unit (almost always a service) on a schedule. Think of it as cron’s modern replacement that is built right into Ubuntu. Because timers are part of systemd (the system and service manager that boots your server and runs background services), they get proper logging, dependency tracking, and resource control for free — things plain cron simply cannot do. If you are writing automation on a modern Ubuntu 22.04 or 24.04 LTS server, timers are usually the better choice.
How timers work: the .timer + .service pair
A timer never does the actual work itself. It is a trigger. The real work lives in a separate .service unit. You always create two files that share the same base name:
mybackup.service— what to run (the command).mybackup.timer— when to run it (the schedule).
systemd automatically pairs them by name. When mybackup.timer fires, it starts mybackup.service. This separation is a feature: you can trigger the same service manually for testing without waiting for the schedule.
Unit files for system-wide tasks live in /etc/systemd/system/. Let’s build a real backup job.
Step 1: create the service
Create /etc/systemd/system/dbbackup.service:
[Unit]
Description=Back up the application database
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/dbbackup.sh
User=postgres
Type=oneshot tells systemd this is a short task that runs and exits (not a long-running daemon, which is a program that stays running in the background). User=postgres runs the script as the postgres user. The Wants/After lines mean “don’t run until the network is actually up” — a dependency cron cannot express.
Create the script and make it executable:
sudo tee /usr/local/bin/dbbackup.sh > /dev/null <<'EOF'
#!/bin/bash
set -euo pipefail
pg_dumpall | gzip > "/var/backups/db-$(date +%F).sql.gz"
EOF
sudo chmod +x /usr/local/bin/dbbackup.sh
sudo mkdir -p /var/backups
Step 2: create the timer
Create /etc/systemd/system/dbbackup.timer:
[Unit]
Description=Run the database backup daily
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true is one of the best timer features: if the server was powered off at 02:30, the job runs as soon as it boots back up. Cron would just silently skip the missed run.
Step 3: enable and start the timer
After creating or editing any unit file, reload systemd so it sees your changes, then enable the timer (not the service):
sudo systemctl daemon-reload
sudo systemctl enable --now dbbackup.timer
Output:
Created symlink /etc/systemd/system/timers.target.wants/dbbackup.timer → /etc/systemd/system/dbbackup.timer.
enable makes it survive reboots; --now also starts it immediately.
OnCalendar syntax
OnCalendar defines when the timer fires. The format is DayOfWeek Year-Month-Day Hour:Minute:Second. An asterisk * means “every”, and you can use lists, ranges, and steps.
| Goal | OnCalendar value |
|---|---|
| Every day at 02:30 | *-*-* 02:30:00 |
| Every hour, on the hour | *-*-* *:00:00 |
| Every 15 minutes | *:0/15 |
| Every Monday at 09:00 | Mon *-*-* 09:00:00 |
| Weekdays at 18:00 | Mon..Fri *-*-* 18:00:00 |
| First day of each month, midnight | *-*-01 00:00:00 |
| Shorthand keywords | daily, weekly, monthly, hourly |
Never guess the syntax — test it. The systemd-analyze calendar command parses an expression and shows the next few times it will fire:
systemd-analyze calendar --iterations=3 "Mon..Fri *-*-* 18:00:00"
Output:
Original form: Mon..Fri *-*-* 18:00:00
Normalized form: Mon..Fri *-*-* 18:00:00
Next elapse: Mon 2026-06-15 18:00:00 UTC
(in UTC): Mon 2026-06-15 18:00:00 UTC
From now: 7h 12min left
Iteration 2: Tue 2026-06-16 18:00:00 UTC
Iteration 3: Wed 2026-06-17 18:00:00 UTC
Tip: Set
RandomizedDelaySpec=in the[Timer]section (for exampleRandomizedDelaySec=5m) to spread load. If 50 servers all run a job at exactly 02:00, they may overwhelm a shared resource. A random delay staggers them.
Listing and inspecting timers
To see every active timer, when it last ran, and when it fires next:
systemctl list-timers
Output:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Mon 2026-06-15 02:30:00 UTC 18h left Sun 2026-06-14 02:30:00 UTC 5h ago dbbackup.timer dbbackup.service
Mon 2026-06-15 06:00:00 UTC 22h left Sun 2026-06-14 06:00:00 UTC 1h ago apt-daily.timer apt-daily.service
Add --all to include inactive timers. To test the job right now without waiting, start the service directly:
sudo systemctl start dbbackup.service
This is a huge advantage over cron: there is no “comment out the schedule and wait” dance.
Reading the logs
Every run is captured by the journal (systemd’s central log store). To see output and errors from your job:
journalctl -u dbbackup.service --since today
Output:
Jun 14 02:30:00 web01 systemd[1]: Starting Back up the application database...
Jun 14 02:30:04 web01 systemd[1]: dbbackup.service: Deactivated successfully.
Jun 14 02:30:04 web01 systemd[1]: Finished Back up the application database.
With cron, output goes to local mail or /dev/null unless you set up redirection yourself. With timers, stdout and stderr are logged automatically and are searchable by unit, time, and exit status.
Timers vs cron — when to use which
| Feature | systemd timer | cron |
|---|---|---|
| Logging | Automatic, via journalctl -u | Manual; you must redirect output |
| Missed runs (downtime) | Persistent=true catches up | Silently skipped |
| Dependencies | After=network-online.target, etc. | None |
| Resource limits | CPUQuota=, MemoryMax= on the service | None |
| Run job manually to test | systemctl start svc.service | Awkward |
| Setup effort | Two files + reload | One crontab line |
| Per-user simple jobs | Possible but verbose | Very quick (crontab -e) |
Prefer timers when: the job matters (backups, deploys, cleanup), needs reliable logging, depends on the network or another service, or must catch up after downtime.
Cron is still fine when: you need a quick one-line personal job and don’t care about logging or missed runs.
Best Practices
- Always name the
.timerand.servicefiles with the same base name so systemd pairs them automatically. - Run
sudo systemctl daemon-reloadafter every edit to a unit file, or your changes are ignored. - Use
Type=oneshotfor scheduled scripts that run and exit — neverType=simple. - Set
Persistent=truefor jobs that must not be skipped if the server was off. - Test the schedule with
systemd-analyze calendar "..."before enabling the timer. - Add
RandomizedDelaySec=to stagger jobs across a fleet of servers. - Run jobs as a dedicated low-privilege user with
User=instead of root whenever possible.