Innovate | Connect | Transform
ContactService statusUSD $

GICT knowledge base

How to Schedule Tasks with systemd Timers

Published

Linux Administration
How to Schedule Tasks with systemd Timers

A scheduled script needs more than a start time. You need to know whether it ran, whether it failed, and what happens when the VPS is offline at the scheduled hour. A systemd timer handles the schedule while a separate service runs the command and records its output in the journal.

Check the server and existing schedules

Use an Ubuntu 24.04 VPS with systemd running as PID 1 and an account with sudo access. No extra scheduler package, domain, or open network port is needed. Run every command on that VPS.

$ systemctl --version
$ timedatectl status
$ systemctl list-timers --all --no-pager
$ systemctl cat kb-timestamp.service kb-timestamp.timer

Check that the clock is correct before scheduling work. The last command should report that these example units do not exist. If either already exists, inspect it and choose another matching service/timer name rather than overwriting it. Leave unrelated timers and cron jobs alone.

Calendar schedules follow wall-clock time. The examples specify UTC explicitly, so changing the server’s local timezone does not move the scheduled hour. This is different from a monotonic timer such as OnBootSec=5min, which measures an interval after boot.

Create the task and its service

Create /usr/local/bin/kb-timestamp with a sudo-capable text editor. Check first that the path is not already used by another script. Save these contents:

#!/bin/sh
set -eu
/usr/bin/date -u '+timer-run %Y-%m-%dT%H:%M:%SZ'

Make the script root-owned and executable. The service user will be able to run it but not change it:

$ sudo chown root:root /usr/local/bin/kb-timestamp
$ sudo chmod 0755 /usr/local/bin/kb-timestamp

Create /etc/systemd/system/kb-timestamp.service:

[Unit]
Description=Write a scheduled timestamp to the journal

[Service]
Type=oneshot
DynamicUser=yes
ExecStart=/usr/local/bin/kb-timestamp
StandardOutput=journal
StandardError=journal
TimeoutStartSec=30s

Type=oneshot waits for the script to finish. DynamicUser=yes gives this simple task a temporary unprivileged identity. It does not need a home directory or permission to write application files. The timestamp goes to standard output, which systemd sends to the journal.

Do not add RemainAfterExit=yes: a service left active after completion will not be started again by a repetitive timer. The 30-second timeout bounds this demonstration; choose a realistic timeout for a real task rather than copying it into a long-running backup job.

Add and verify the timer

Create /etc/systemd/system/kb-timestamp.timer:

[Unit]
Description=Run the timestamp task every minute

[Timer]
OnCalendar=*-*-* *:*:00 UTC
AccuracySec=1s
Persistent=true

[Install]
WantedBy=timers.target

Matching base names connect kb-timestamp.timer to kb-timestamp.service; an explicit Unit= setting is unnecessary. The calendar expression means the start of every minute, not 60 seconds after the preceding task finishes.

AccuracySec=1s narrows the scheduling window from the one-minute default. It does not make Linux a real-time system or guarantee execution at an exact millisecond under load. No random delay is configured here.

Validate both files and preview the next three calendar events:

$ sudo systemd-analyze verify /etc/systemd/system/kb-timestamp.service /etc/systemd/system/kb-timestamp.timer
$ systemd-analyze calendar --iterations=3 '*-*-* *:*:00 UTC'
$ sudo systemctl daemon-reload
$ sudo systemctl start kb-timestamp.service
$ sudo systemctl show kb-timestamp.service -p Result -p ExecMainStatus -p ActiveState

The successful manual run should return:

Result=success
ExecMainStatus=0
ActiveState=inactive

Read the corresponding output:

$ sudo journalctl -u kb-timestamp.service -n 10 --no-pager

Resolve validation errors before proceeding. The manual run separates script failures from scheduling problems. A successful oneshot normally returns to inactive, with Result=success and ExecMainStatus=0. Inactive does not mean this completed task failed. The journal should contain a timer-run line with the current UTC timestamp.

Now enable the timer at boot and start it immediately:

$ sudo systemctl enable --now kb-timestamp.timer
$ systemctl list-timers kb-timestamp.timer --all --no-pager
$ systemctl is-enabled kb-timestamp.timer

Enable the timer, not the service. Wait for two minute boundaries, then read its latest timestamp messages:

$ sudo journalctl -u kb-timestamp.service -o cat --grep '^timer-run ' -n 2 --no-pager

Look for distinct timer-run entries with recent timestamps. The service output, not the timer’s waiting state, establishes that the command ran.

Your timestamps will differ. NEXT and LAST in list-timers describe timer activation; only the service result and its output prove that the task completed.

Verify missed-run handling

After at least one timer-triggered run, stop only this timer:

$ sudo systemctl stop kb-timestamp.timer

Leave it stopped for just over two minutes. Confirm that no new timer-run entries appear during that interval, then reactivate it between minute boundaries:

$ sudo systemctl start kb-timestamp.timer
$ sudo journalctl -u kb-timestamp.service --since '5 minutes ago' --no-pager
$ systemctl list-timers kb-timestamp.timer --all --no-pager

Persistent=true catches up once when a calendar timer becomes active after missing one or more scheduled events. Look for one immediate timestamp, followed by normal minute-boundary runs. Starting near the next boundary can place those two different activations close together.

The persistent timestamp also allows catch-up after the machine has been powered off, once the enabled timer starts again. It is not a queue of every missed execution and does not rerun a job simply because its last attempt failed. Persistent= has no effect on monotonic settings such as OnBootSec= or OnUnitActiveSec=.

Switch to a daily schedule

After the demonstration, stop the timer and replace only its example file with the following contents. The script and service stay unchanged.

$ sudo systemctl stop kb-timestamp.timer

File: /etc/systemd/system/kb-timestamp.timer

[Unit]
Description=Run the timestamp task daily at 03:15 UTC

[Timer]
OnCalendar=*-*-* 03:15:00 UTC
AccuracySec=1s
Persistent=true

[Install]
WantedBy=timers.target

Validate the updated schedule, reload the unit definitions, and start the timer:

$ sudo systemd-analyze verify /etc/systemd/system/kb-timestamp.service /etc/systemd/system/kb-timestamp.timer
$ systemd-analyze calendar --iterations=3 '*-*-* 03:15:00 UTC'
$ sudo systemctl daemon-reload
$ sudo systemctl start kb-timestamp.timer
$ systemctl list-timers kb-timestamp.timer --all --no-pager

Check the actual next time instead of assuming the edit took effect. Because the timer remains persistent, reactivation can also cause an immediate catch-up if the revised schedule has a missed event since its last recorded activation.

For a real task, replace the script contents and review its execution identity, file permissions, timeout, and dependencies. System services do not inherit your interactive shell configuration. Use absolute paths and explicitly provide any required environment. A command needing persistent storage or a database account needs permissions suited to that application; the temporary identity in this example is deliberately limited.

To retire this demonstration without deleting its files:

$ sudo systemctl disable --now kb-timestamp.timer

Stopping a timer prevents future activations; it does not cancel a service that is already running. This timestamp task normally finishes immediately.

If the timer waits but the job fails

  • Read the service journal. active (waiting) describes the timer, not the last task result. Inspect Result and ExecMainStatus on the service. A nonzero exit can coexist with a timer still waiting for its next activation.
  • Check the executable path. A missing file, wrong shebang, or missing execute permission prevents the task from starting. The journal identifies the failure; fix the script, then repeat the manual service test.
  • Reload edited units. Saving a unit file does not update the manager’s loaded definition. Run daemon-reload, then restart the timer when changing its schedule. systemctl cat kb-timestamp.service kb-timestamp.timer also exposes drop-ins that might alter your configuration.
  • Allow the service to finish. A timer does not spawn a second copy of the same service while it is active. It is not a backlog processor for work that consistently exceeds the schedule interval.
  • Do not expect automatic retries. A failed service can leave the timer waiting normally for its next event. After fixing the failure, start the service manually to confirm recovery; use the journal for the failure history.

When cron is enough

An existing, reliable cron entry does not need migration just to use a newer interface. Cron is shorter for a simple recurring command and works beyond systemd-based Linux. Timers are useful when service status, journal output, explicit execution settings, or calendar catch-up justify maintaining two unit files.

Neither scheduler guarantees exactly-once application transactions. A job can partly complete before failing; design reruns to be safe and use application checkpoints when every operation matters. Verify the minute and catch-up behavior yourself before switching this example to the daily 03:15 UTC schedule.

Reference: systemd timer behavior. Check the documentation for your installed version before applying changes.

Related guides

Account services are unavailable until the production backend is configured.