Skip to content

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 systemd on a systemd system and init on Gentoo with OpenRC (where sysvinit is PID 1):

    $ ps -p 1 -o comm=
    systemd
    
  • 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 /
sudo chmod 0755 /usr/local/bin/disk-usage-report

Then create and test the units. The steps are the same on every systemd distribution.

  1. 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-report
    

    Type=oneshot tells 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. Add User= to run it as an unprivileged user, as described in systemd unit directives.

  2. Load the unit, run it once by hand, and read its output in the journal:

    sudo systemctl daemon-reload
    sudo systemctl start disk-usage-report.service
    
    $ 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 start waits until a oneshot service has finished, and fails if the command fails. Everything the command prints goes to the journal, tagged with the unit name.

  3. 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.target
    

    A timer starts the service with the same name by default (disk-usage-report.timer starts disk-usage-report.service). AccuracySec=1s makes 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:

    sudo systemctl daemon-reload
    sudo systemctl enable --now disk-usage-report.timer
    
  4. 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 LAST column shows when the timer last started the service, and the journal shows that run's output.

  5. 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.target
    
    • Persistent=true records each run on disk. If the machine was off at 02:30, the job runs once shortly after the next boot.
    • RandomizedDelaySec=10min delays 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 calendar shows 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 left
    

    The expression syntax is described in systemd timer directives. Reload systemd and restart the timer so it uses the new schedule:

    sudo systemctl daemon-reload
    sudo systemctl restart disk-usage-report.timer
    
    $ 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.
    

    NEXT shows the next run, including its random delay.

  1. 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-report
    

    Type=oneshot tells 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. Add User= to run it as an unprivileged user, as described in systemd unit directives.

  2. Load the unit, run it once by hand, and read its output in the journal:

    sudo systemctl daemon-reload
    sudo systemctl start disk-usage-report.service
    
    $ 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 start waits until a oneshot service has finished, and fails if the command fails. Everything the command prints goes to the journal, tagged with the unit name.

  3. 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.target
    

    A timer starts the service with the same name by default (disk-usage-report.timer starts disk-usage-report.service). AccuracySec=1s makes 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:

    sudo systemctl daemon-reload
    sudo systemctl enable --now disk-usage-report.timer
    
  4. 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 LAST column shows when the timer last started the service, and the journal shows that run's output.

  5. 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.target
    
    • Persistent=true records each run on disk. If the machine was off at 02:30, the job runs once shortly after the next boot.
    • RandomizedDelaySec=10min delays 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 calendar shows 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 left
    

    The expression syntax is described in systemd timer directives. Reload systemd and restart the timer so it uses the new schedule:

    sudo systemctl daemon-reload
    sudo systemctl restart disk-usage-report.timer
    
    $ 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.
    

    NEXT shows the next run, including its random delay.

  1. 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-report
    

    Type=oneshot tells 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. Add User= to run it as an unprivileged user, as described in systemd unit directives.

  2. Load the unit, run it once by hand, and read its output in the journal:

    sudo systemctl daemon-reload
    sudo systemctl start disk-usage-report.service
    
    $ 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 start waits until a oneshot service has finished, and fails if the command fails. Everything the command prints goes to the journal, tagged with the unit name.

  3. 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.target
    

    A timer starts the service with the same name by default (disk-usage-report.timer starts disk-usage-report.service). AccuracySec=1s makes 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:

    sudo systemctl daemon-reload
    sudo systemctl enable --now disk-usage-report.timer
    
  4. 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 LAST column shows when the timer last started the service, and the journal shows that run's output.

  5. 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.target
    
    • Persistent=true records each run on disk. If the machine was off at 02:30, the job runs once shortly after the next boot.
    • RandomizedDelaySec=10min delays 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 calendar shows 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 left
    

    The expression syntax is described in systemd timer directives. Reload systemd and restart the timer so it uses the new schedule:

    sudo systemctl daemon-reload
    sudo systemctl restart disk-usage-report.timer
    
    $ 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.
    

    NEXT shows 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.