How to schedule a job with a systemd timer¶
You have a command that should run on a schedule, for example a report every night at 02:30, on a system managed by systemd. This guide wraps the command in a service unit, adds a timer unit that starts it, proves that the timer fires with a test schedule of every minute, and then sets the real schedule. Compared with cron, the job's output and exit status end up in the journal, a run missed while the machine was off can be caught up after the next boot, and runs can be spread out with a random delay; cron and systemd timers discusses the differences.
The example job prints the disk usage of /.
Replace it with your own command.
Before you begin¶
- You need root access through
sudo. -
The machine must run systemd. This command prints
systemdon a systemd system andiniton Gentoo with OpenRC (where sysvinit is PID 1): -
Timers work on every Debian, Ubuntu, RHEL, AlmaLinux, Rocky Linux and Arch system; nothing needs installing. OpenRC has no timer units: on Gentoo with OpenRC, use How to schedule a job with cron instead.
Steps¶
Create the command the job runs, /usr/local/bin/disk-usage-report:
#!/bin/sh
# disk-usage-report: print a timestamp and the disk usage of /.
set -eu
date -Iseconds
df -h /
Then create and test the units. The steps are the same on every systemd distribution.
-
Create a service unit that runs the command once,
/etc/systemd/system/disk-usage-report.service:[Unit] Description=Write a disk usage report [Service] Type=oneshot ExecStart=/usr/local/bin/disk-usage-reportType=oneshottells systemd that the command runs to completion and exits, rather than staying in the background. The service has no[Install]section: the timer starts it, so it isn't enabled on its own. AddUser=to run it as an unprivileged user, as described in systemd unit directives. -
Load the unit, run it once by hand, and read its output in the journal:
$ systemctl show disk-usage-report.service --property=Result,ExecMainStatus Result=success ExecMainStatus=0 $ sudo journalctl -u disk-usage-report.service -n 5 --no-pager Nov 07 14:30:12 server1 disk-usage-report[1873]: 2026-11-07T14:30:12+00:00 Nov 07 14:30:12 server1 disk-usage-report[1874]: Filesystem Size Used Avail Use% Mounted on Nov 07 14:30:12 server1 disk-usage-report[1874]: /dev/vda1 9.7G 1.9G 7.4G 21% / Nov 07 14:30:12 server1 systemd[1]: disk-usage-report.service: Deactivated successfully. Nov 07 14:30:12 server1 systemd[1]: Finished disk-usage-report.service - Write a disk usage report.systemctl startwaits until aoneshotservice has finished, and fails if the command fails. Everything the command prints goes to the journal, tagged with the unit name. -
Create a timer unit with the same name,
/etc/systemd/system/disk-usage-report.timer, with a test schedule of every minute:[Unit] Description=Run disk-usage-report (test schedule: every minute) [Timer] OnCalendar=minutely AccuracySec=1s [Install] WantedBy=timers.targetA timer starts the service with the same name by default (
disk-usage-report.timerstartsdisk-usage-report.service).AccuracySec=1smakes it fire within a second of each minute; the default lets systemd delay it by up to a minute.Load it, enable it so that it's started at every boot, and start it now:
-
Wait until the next minute has passed, then check that the timer fired:
$ sleep 65 $ systemctl list-timers disk-usage-report.timer --no-pager NEXT LEFT LAST PASSED UNIT ACTIVATES Sat 2026-11-07 14:33:00 UTC 41s Sat 2026-11-07 14:32:00 UTC 18s ago disk-usage-report.timer disk-usage-report.service 1 timers listed. Pass --all to see loaded but inactive timers, too. $ sudo journalctl -u disk-usage-report.service -n 5 --no-pager Nov 07 14:32:00 server1 disk-usage-report[1902]: 2026-11-07T14:32:00+00:00 Nov 07 14:32:00 server1 disk-usage-report[1903]: Filesystem Size Used Avail Use% Mounted on Nov 07 14:32:00 server1 disk-usage-report[1903]: /dev/vda1 9.7G 1.9G 7.4G 21% / Nov 07 14:32:00 server1 systemd[1]: disk-usage-report.service: Deactivated successfully. Nov 07 14:32:00 server1 systemd[1]: Finished disk-usage-report.service - Write a disk usage report.The
LASTcolumn shows when the timer last started the service, and the journal shows that run's output. -
Set the real schedule. Replace the timer with this version, which runs the job every day at 02:30:
[Unit] Description=Run disk-usage-report every day at 02:30 [Timer] OnCalendar=*-*-* 02:30:00 Persistent=true RandomizedDelaySec=10min [Install] WantedBy=timers.targetPersistent=truerecords each run on disk. If the machine was off at 02:30, the job runs once shortly after the next boot.RandomizedDelaySec=10mindelays each run by a random time between 0 and 10 minutes, so that many machines with the same timer don't all start at once. Leave it out if the job must start on time.
Check the calendar expression before you rely on it.
systemd-analyze calendarshows its normalised form and when it next elapses:$ systemd-analyze calendar '*-*-* 02:30:00' Original form: *-*-* 02:30:00 Normalized form: *-*-* 02:30:00 Next elapse: Sun 2026-11-08 02:30:00 UTC From now: 11h leftThe expression syntax is described in systemd timer directives. Reload systemd and restart the timer so it uses the new schedule:
$ systemctl list-timers disk-usage-report.timer --no-pager NEXT LEFT LAST PASSED UNIT ACTIVATES Sun 2026-11-08 02:34:51 UTC 11h Sat 2026-11-07 14:33:00 UTC 2min ago disk-usage-report.timer disk-usage-report.service 1 timers listed. Pass --all to see loaded but inactive timers, too.NEXTshows the next run, including its random delay.
-
Create a service unit that runs the command once,
/etc/systemd/system/disk-usage-report.service:[Unit] Description=Write a disk usage report [Service] Type=oneshot ExecStart=/usr/local/bin/disk-usage-reportType=oneshottells systemd that the command runs to completion and exits, rather than staying in the background. The service has no[Install]section: the timer starts it, so it isn't enabled on its own. AddUser=to run it as an unprivileged user, as described in systemd unit directives. -
Load the unit, run it once by hand, and read its output in the journal:
$ systemctl show disk-usage-report.service --property=Result,ExecMainStatus Result=success ExecMainStatus=0 $ sudo journalctl -u disk-usage-report.service -n 5 --no-pager Nov 07 14:30:12 server1 disk-usage-report[1873]: 2026-11-07T14:30:12+00:00 Nov 07 14:30:12 server1 disk-usage-report[1874]: Filesystem Size Used Avail Use% Mounted on Nov 07 14:30:12 server1 disk-usage-report[1874]: /dev/vda1 9.7G 1.9G 7.4G 21% / Nov 07 14:30:12 server1 systemd[1]: disk-usage-report.service: Deactivated successfully. Nov 07 14:30:12 server1 systemd[1]: Finished disk-usage-report.service - Write a disk usage report.systemctl startwaits until aoneshotservice has finished, and fails if the command fails. Everything the command prints goes to the journal, tagged with the unit name. -
Create a timer unit with the same name,
/etc/systemd/system/disk-usage-report.timer, with a test schedule of every minute:[Unit] Description=Run disk-usage-report (test schedule: every minute) [Timer] OnCalendar=minutely AccuracySec=1s [Install] WantedBy=timers.targetA timer starts the service with the same name by default (
disk-usage-report.timerstartsdisk-usage-report.service).AccuracySec=1smakes it fire within a second of each minute; the default lets systemd delay it by up to a minute.Load it, enable it so that it's started at every boot, and start it now:
-
Wait until the next minute has passed, then check that the timer fired:
$ sleep 65 $ systemctl list-timers disk-usage-report.timer --no-pager NEXT LEFT LAST PASSED UNIT ACTIVATES Sat 2026-11-07 14:33:00 UTC 41s Sat 2026-11-07 14:32:00 UTC 18s ago disk-usage-report.timer disk-usage-report.service 1 timers listed. Pass --all to see loaded but inactive timers, too. $ sudo journalctl -u disk-usage-report.service -n 5 --no-pager Nov 07 14:32:00 server1 disk-usage-report[1902]: 2026-11-07T14:32:00+00:00 Nov 07 14:32:00 server1 disk-usage-report[1903]: Filesystem Size Used Avail Use% Mounted on Nov 07 14:32:00 server1 disk-usage-report[1903]: /dev/vda1 9.7G 1.9G 7.4G 21% / Nov 07 14:32:00 server1 systemd[1]: disk-usage-report.service: Deactivated successfully. Nov 07 14:32:00 server1 systemd[1]: Finished disk-usage-report.service - Write a disk usage report.The
LASTcolumn shows when the timer last started the service, and the journal shows that run's output. -
Set the real schedule. Replace the timer with this version, which runs the job every day at 02:30:
[Unit] Description=Run disk-usage-report every day at 02:30 [Timer] OnCalendar=*-*-* 02:30:00 Persistent=true RandomizedDelaySec=10min [Install] WantedBy=timers.targetPersistent=truerecords each run on disk. If the machine was off at 02:30, the job runs once shortly after the next boot.RandomizedDelaySec=10mindelays each run by a random time between 0 and 10 minutes, so that many machines with the same timer don't all start at once. Leave it out if the job must start on time.
Check the calendar expression before you rely on it.
systemd-analyze calendarshows its normalised form and when it next elapses:$ systemd-analyze calendar '*-*-* 02:30:00' Original form: *-*-* 02:30:00 Normalized form: *-*-* 02:30:00 Next elapse: Sun 2026-11-08 02:30:00 UTC From now: 11h leftThe expression syntax is described in systemd timer directives. Reload systemd and restart the timer so it uses the new schedule:
$ systemctl list-timers disk-usage-report.timer --no-pager NEXT LEFT LAST PASSED UNIT ACTIVATES Sun 2026-11-08 02:34:51 UTC 11h Sat 2026-11-07 14:33:00 UTC 2min ago disk-usage-report.timer disk-usage-report.service 1 timers listed. Pass --all to see loaded but inactive timers, too.NEXTshows the next run, including its random delay.
-
Create a service unit that runs the command once,
/etc/systemd/system/disk-usage-report.service:[Unit] Description=Write a disk usage report [Service] Type=oneshot ExecStart=/usr/local/bin/disk-usage-reportType=oneshottells systemd that the command runs to completion and exits, rather than staying in the background. The service has no[Install]section: the timer starts it, so it isn't enabled on its own. AddUser=to run it as an unprivileged user, as described in systemd unit directives. -
Load the unit, run it once by hand, and read its output in the journal:
$ systemctl show disk-usage-report.service --property=Result,ExecMainStatus Result=success ExecMainStatus=0 $ sudo journalctl -u disk-usage-report.service -n 5 --no-pager Nov 07 14:30:12 server1 disk-usage-report[1873]: 2026-11-07T14:30:12+00:00 Nov 07 14:30:12 server1 disk-usage-report[1874]: Filesystem Size Used Avail Use% Mounted on Nov 07 14:30:12 server1 disk-usage-report[1874]: /dev/vda1 9.7G 1.9G 7.4G 21% / Nov 07 14:30:12 server1 systemd[1]: disk-usage-report.service: Deactivated successfully. Nov 07 14:30:12 server1 systemd[1]: Finished disk-usage-report.service - Write a disk usage report.systemctl startwaits until aoneshotservice has finished, and fails if the command fails. Everything the command prints goes to the journal, tagged with the unit name. -
Create a timer unit with the same name,
/etc/systemd/system/disk-usage-report.timer, with a test schedule of every minute:[Unit] Description=Run disk-usage-report (test schedule: every minute) [Timer] OnCalendar=minutely AccuracySec=1s [Install] WantedBy=timers.targetA timer starts the service with the same name by default (
disk-usage-report.timerstartsdisk-usage-report.service).AccuracySec=1smakes it fire within a second of each minute; the default lets systemd delay it by up to a minute.Load it, enable it so that it's started at every boot, and start it now:
-
Wait until the next minute has passed, then check that the timer fired:
$ sleep 65 $ systemctl list-timers disk-usage-report.timer --no-pager NEXT LEFT LAST PASSED UNIT ACTIVATES Sat 2026-11-07 14:33:00 UTC 41s Sat 2026-11-07 14:32:00 UTC 18s ago disk-usage-report.timer disk-usage-report.service 1 timers listed. Pass --all to see loaded but inactive timers, too. $ sudo journalctl -u disk-usage-report.service -n 5 --no-pager Nov 07 14:32:00 server1 disk-usage-report[1902]: 2026-11-07T14:32:00+00:00 Nov 07 14:32:00 server1 disk-usage-report[1903]: Filesystem Size Used Avail Use% Mounted on Nov 07 14:32:00 server1 disk-usage-report[1903]: /dev/vda1 9.7G 1.9G 7.4G 21% / Nov 07 14:32:00 server1 systemd[1]: disk-usage-report.service: Deactivated successfully. Nov 07 14:32:00 server1 systemd[1]: Finished disk-usage-report.service - Write a disk usage report.The
LASTcolumn shows when the timer last started the service, and the journal shows that run's output. -
Set the real schedule. Replace the timer with this version, which runs the job every day at 02:30:
[Unit] Description=Run disk-usage-report every day at 02:30 [Timer] OnCalendar=*-*-* 02:30:00 Persistent=true RandomizedDelaySec=10min [Install] WantedBy=timers.targetPersistent=truerecords each run on disk. If the machine was off at 02:30, the job runs once shortly after the next boot.RandomizedDelaySec=10mindelays each run by a random time between 0 and 10 minutes, so that many machines with the same timer don't all start at once. Leave it out if the job must start on time.
Check the calendar expression before you rely on it.
systemd-analyze calendarshows its normalised form and when it next elapses:$ systemd-analyze calendar '*-*-* 02:30:00' Original form: *-*-* 02:30:00 Normalized form: *-*-* 02:30:00 Next elapse: Sun 2026-11-08 02:30:00 UTC From now: 11h leftThe expression syntax is described in systemd timer directives. Reload systemd and restart the timer so it uses the new schedule:
$ systemctl list-timers disk-usage-report.timer --no-pager NEXT LEFT LAST PASSED UNIT ACTIVATES Sun 2026-11-08 02:34:51 UTC 11h Sat 2026-11-07 14:33:00 UTC 2min ago disk-usage-report.timer disk-usage-report.service 1 timers listed. Pass --all to see loaded but inactive timers, too.NEXTshows the next run, including its random delay.
OpenRC has no timer units, so this section doesn't apply.
Schedule the job with cron instead, as described in How to schedule a job with cron; for a daily job at 02:30, the crontab line is 30 2 * * * root /usr/local/bin/disk-usage-report.
With a systemd profile, follow the steps in the other tabs unchanged (systemd profile, not verified).
Verify¶
Check that the timer is enabled and active, and that the service's last run succeeded:
$ systemctl is-enabled disk-usage-report.timer
enabled
$ systemctl is-active disk-usage-report.timer
active
$ systemctl show disk-usage-report.service --property=Result,ExecMainStatus
Result=success
ExecMainStatus=0
After each scheduled run, Result=success and exit status 0 confirm that the job succeeded, and journalctl -u disk-usage-report.service shows its output.
systemctl status disk-usage-report.service shows the same with more detail; it exits with status 3 because a oneshot service is inactive between runs, which is normal.
$ systemctl is-enabled disk-usage-report.timer
enabled
$ systemctl is-active disk-usage-report.timer
active
$ systemctl show disk-usage-report.service --property=Result,ExecMainStatus
Result=success
ExecMainStatus=0
After each scheduled run, Result=success and exit status 0 confirm that the job succeeded, and journalctl -u disk-usage-report.service shows its output.
systemctl status disk-usage-report.service shows the same with more detail; it exits with status 3 because a oneshot service is inactive between runs, which is normal.
$ systemctl is-enabled disk-usage-report.timer
enabled
$ systemctl is-active disk-usage-report.timer
active
$ systemctl show disk-usage-report.service --property=Result,ExecMainStatus
Result=success
ExecMainStatus=0
After each scheduled run, Result=success and exit status 0 confirm that the job succeeded, and journalctl -u disk-usage-report.service shows its output.
systemctl status disk-usage-report.service shows the same with more detail; it exits with status 3 because a oneshot service is inactive between runs, which is normal.
Not applicable with OpenRC; see How to schedule a job with cron.
Troubleshooting¶
The timer is listed but LAST stays empty (-).
The timer hasn't elapsed since it was started, or the schedule isn't what you think.
Check the expression with systemd-analyze calendar '<EXPRESSION>' (where <EXPRESSION> is your OnCalendar= value) and look at NEXT in systemctl list-timers.
After editing a timer file, run sudo systemctl daemon-reload and restart the timer; until then, systemd keeps using the old schedule.
The timer isn't listed at all, or is inactive after a reboot.
It was started but not enabled, or its [Install] section is missing.
Run sudo systemctl enable --now disk-usage-report.timer and check that the timer file has WantedBy=timers.target.
Failed to start disk-usage-report.timer: Unit disk-usage-report.timer has a bad unit file setting.
Run sudo systemd-analyze verify /etc/systemd/system/disk-usage-report.timer to see which line is wrong; an invalid OnCalendar= expression is a common cause.
The service fails when the timer runs it, but works in your shell.
Read its output with sudo journalctl -u disk-usage-report.service.
Services run with a minimal environment: use absolute paths in the command and set the variables it needs with Environment= in the service unit.
systemctl status disk-usage-report.service shows the exit status of the last run.
The job runs some seconds or minutes late.
That's AccuracySec= (one minute by default) and, if set, RandomizedDelaySec=.
Set AccuracySec=1s if the job must start on time.
Related¶
- Reference: systemd timer directives
- Reference: systemd unit directives
- Explanation: cron and systemd timers
- How-to: How to schedule a job with cron
- How-to: How to write a safe shell script
- How-to: How to run a script as a service
- Tutorial: Configure a server with Ansible