Where to start First steps track · 15 stages
Linux Labs

systemd timers — scheduled jobs

11 min Ubuntu 24.04 · systemd 255

A systemd timer is a .timer unit that starts the service of the same name on a schedule; its output goes to the journal.

Cheat sheet

8 commands Printable version
  • systemctl list-timers timers and their next run
  • systemctl list-timers --all inactive ones too
  • sudo systemctl enable --now clean.timer enable and start the timer
  • sudo systemctl start clean.service run the job right now
  • journalctl -u clean.service -n 20 output of the latest runs
  • systemd-analyze calendar 'Mon..Fri 03:00' check a schedule
  • systemctl cat clean.timer what the unit file says
  • sudo systemctl daemon-reload after editing unit files

Overview

A timer is a systemd unit with the .timer extension. It runs nothing itself: at the set time it starts a service, by default the one with the same name (clean.timer → clean.service). So a job is a pair of files in /etc/systemd/system: the service says what to do, the timer says when.

ini
# /etc/systemd/system/clean.service
[Unit]
Description=Cleanup old exports

[Service]
Type=oneshot
ExecStart=/usr/local/bin/clean.sh
ini
# /etc/systemd/system/clean.timer
[Unit]
Description=Run cleanup daily

[Timer]
OnCalendar=*-*-* 03:30
Persistent=true

[Install]
WantedBy=timers.target

Type=oneshot is a service that runs a command and finishes; while the script runs, the service is activating. You enable the timer: the service has no [Install] section, and systemctl enable clean.service only prints a long warning. An enabled timer shows up in the list with its next run time:

text
student@ubuntu:~$ systemctl list-timers clean.timer --no-pager
NEXT                        LEFT LAST PASSED UNIT        ACTIVATES
Wed 2026-10-21 03:30:00 UTC  17h -         - clean.timer clean.service
cron systemd timer
job output mail, or lost the service journal, journalctl -u
the machine was off the run is skipped Persistent=true runs it after boot
the previous run is still going a second one starts the same unit never starts twice
run outside the schedule paste the command into a terminal systemctl start, same conditions

The OnCalendar schedule

The full form is DayOfWeek Year-Month-Day Hour:Minute:Second. Any part can be *, a comma-separated list, a range with .. or a step with /.

Entry When
hourly every hour at :00
daily every day at 00:00
weekly Mondays at 00:00
*-*-* 03:30 every day at 03:30
Mon..Fri 03:00 weekdays at 03:00
Sat,Sun 10:00 weekends at 10:00
*-*-01 04:00 the first day of every month
*:0/15 every 15 minutes
03:00 Europe/Moscow 03:00 Moscow time

Without a zone, time follows the machine's clock (timedatectl). systemd-analyze calendar checks an entry and shows its next run:

text
student@ubuntu:~$ systemd-analyze calendar 'Mon..Fri 03:00'
  Original form: Mon..Fri 03:00
Normalized form: Mon..Fri *-*-* 03:00:00
    Next elapse: Wed 2026-10-21 03:00:00 UTC
       From now: 16h left

Besides the calendar, a timer can count from events: OnBootSec=5min is five minutes after boot, OnUnitActiveSec=1h is an hour after the service last started.

The service environment

A service starts outside your terminal and gets an environment of its own:

text
student@ubuntu:~$ sudo systemd-run --pipe --wait --quiet \
    sh -c 'echo "PATH=$PATH"; echo "HOME=$HOME"; pwd; id -un'
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
HOME=
/
root
  • the working directory is /, so a relative path in the script starts at the root; WorkingDirectory= sets another one;
  • PATH is short: ~/bin and whatever ~/.bashrc adds are missing;
  • variables from ~/.bashrc and from crontab lines are missing — set them with Environment=NAME=value or EnvironmentFile=;
  • the user is root with an empty HOME; with User=student the service runs as student and gets HOME=/home/student;
  • ~/.profile and ~/.bashrc are not read, and umask is 0022.

The journal

Everything the script prints to stdout and stderr lands in the service journal. The -o cat option drops the time, host name and clean.sh[PID] from each line:

text
student@ubuntu:~$ sudo journalctl -u clean.service -o cat
Starting clean.service - Cleanup old exports...
cleanup started
clean.service: Deactivated successfully.
Finished clean.service - Cleanup old exports.

Recipes

5

Move a job from cron to a timer

  1. The service: Type=oneshot, the full script path in ExecStart=, the variables it needs in Environment=.
  2. A timer with the same schedule: 30 3 * * * in cron is OnCalendar=*-*-* 03:30.
  3. sudo systemctl daemon-reload, then sudo systemctl start clean.service and the journal — make sure the job passes.
  4. sudo systemctl enable --now clean.timer.
  5. Remove the line from the crontab or the file from /etc/cron.d.

Run the job now and check the result

bash
sudo systemctl start clean.service; echo $?
systemctl status clean.service --no-pager
journalctl -u clean.service -n 20 --no-pager

For oneshot, systemctl start waits for the script to finish and returns 0 or 1. status shows the code the script exited with: status=0/SUCCESS.

An hour after the previous run

ini
[Timer]
OnBootSec=5min
OnUnitActiveSec=1h

A temporary timer without files

bash
sudo systemd-run --on-calendar='*:0/5' --unit=tick /bin/true
text
Running timer as unit: tick.timer
Will run service as unit: tick.service

Such a timer lasts until reboot; stop it with systemctl stop tick.timer.

Try a script in the service environment

bash
sudo systemd-run --pipe --wait /usr/local/bin/clean.sh
sudo systemd-run --pipe --wait -p User=student /usr/local/bin/clean.sh

The script runs as a temporary service: directory /, short PATH, root or the user from -p User=. Environment problems show up before the first scheduled run, and systemd-run returns the script's own code:

text
Finished with result: exit-code
Main processes terminated with: code=exited/status=3

Troubleshooting

8

Works by hand, fails from the timer

It is almost always the service environment (see "The service environment" above). The service journal names the cause:

text
student@ubuntu:~$ sudo journalctl -u clean.service -o cat | tail -3
/usr/local/bin/clean.sh: line 2: report-tool: command not found
clean.service: Deactivated successfully.
Finished clean.service - Cleanup old exports.

command not found — use the full path or Environment=PATH=…; a file "not found" by a relative path — WorkingDirectory=; an empty variable — Environment=.

Job for clean.service failed

The script returned non-zero. The service journal says:

text
clean.service: Main process exited, code=exited, status=1/FAILURE
clean.service: Failed with result 'exit-code'.

A script's code is the code of its last command (see exit codes). The service stays failed until the next successful run or systemctl reset-failed.

The timer exists, but the job never runs

systemctl list-timers --all: a dash in the NEXT column means the timer is not running. systemctl is-enabled clean.timer shows disabled — run sudo systemctl enable --now clean.timer.

Refusing to start, unit … to trigger not loaded

The timer has nothing to point at: no service with the same name exists. Give both files the same name or name the service in the timer: Unit=clean.service under [Timer].

Failed to parse calendar specification

OnCalendar= has an error, and the timer will not start. The journal (journalctl -u clean.timer) has two lines: clean.timer:5: Failed to parse calendar specification, ignoring: daily at 3 and Timer unit lacks value setting. Refusing.

Check the entry with systemd-analyze calendar '…' and the whole file with systemd-analyze verify /etc/systemd/system/clean.timer.

Warning: The unit file … changed on disk

The unit file was edited, but systemd still runs the old version — and the old schedule. Run sudo systemctl daemon-reload, then sudo systemctl restart clean.timer.

The unit files have no installation config

enable was called on the service. Enable the timer instead: sudo systemctl enable --now clean.timer.

The timer fired once and then went quiet

The service has RemainAfterExit=yes. After the first run it stays active even though the script has finished:

text
     Active: active (exited) since Tue 2026-10-20 10:36:40 UTC; 46ms ago

The timer's next trigger (and systemctl start) does nothing: the service is already "running". A scheduled job does not need this line — remove it, run daemon-reload and systemctl stop clean.service.

Danger zone

2

The job in both cron and a timer

Once the job moves to a timer, remove it from cron. Otherwise it follows two schedules and the runs can overlap: systemd knows nothing about the cron copy. Guarding different launchers against each other is the job of flock.

Persistent=true after a long downtime

The last run time is stored in /var/lib/systemd/timers/stamp-*.timer. If the machine was off, the missed run happens right after boot — a heavy job (a backup, a cleanup) starts together with the system.

Terms

2
systemd timer
A .timer unit that starts a service, usually the one with the same name, on a schedule or after a delay. Also appears in: Cron, at, flock, restic and 6 more
oneshot service
A systemd service with Type=oneshot: it runs a command and finishes; systemctl start waits for the end and reports success or failure. Also appears in: systemd-analyze: what slows the boot and what is wrong in a unit

See also

  • cron — the classic scheduler and the crontab format.
  • systemd — units and their dependencies.
  • systemctl — starting, enabling and inspecting units.
  • journalctl — the service journal and time filters.
  • flock — guarding against simultaneous runs.
  • exit-codes — why a service is failed and what status=1 means.
  • env-vars — where PATH comes from and why a service lacks yours.
  • timedatectl — the time zone the timer counts in.
Loading…