WIUNIX ACADEMY
Reliable systemd timers: missed runs, repeatable jobs and completion intervals
Build an isolated repeatable oneshot, inspect UTC calendar catch-up, and choose a completion-relative interval when a daily deadline is the wrong fit.
Make this guide yoursYOUR SYSTEM. YOUR STEPS.
Where are you working?
Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.
USE YOUR OWN VALUES
Configure this tutorial
Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.
Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.
Set values for my environment3 editable values
THE OUTCOME
What will you have at the end?
Build an isolated repeatable oneshot, inspect UTC calendar catch-up, and choose a completion-relative interval when a daily deadline is the wrong fit.
Supported operating systems and versions
Covers Ubuntu 26.10/26.04 LTS/24.04 LTS, Debian 13/12, RHEL/Rocky/AlmaLinux 10/9 and rolling Arch/Manjaro. Environment paths distinguish installed-package queries and mandatory-access-control diagnostics. Rolling paths are dated reviews with installed-build checks. These are documented paths, not an all-OS execution claim; a local timer is neither a durable queue nor an application idempotency guarantee.
Before you begin
- A staging Linux host with systemd as PID 1, authorized sudo access and a working sudoedit editor.
- An unused fixture name and a little local state space; existing units and application schedules must not be replaced.
- Familiarity with systemd units, process exit codes and journal inspection; start/stop actions target only this fixture.
Choose the promise your schedule can keep
A daily maintenance task can disappear during downtime, run twice after a retry, or still be running when its next deadline arrives. A timer answers when to request a service start; the job must decide whether work is needed and whether a previous attempt committed its result.
We build a new, isolated daily marker job, inspect its result, and then compare a UTC wall-clock schedule with an interval measured after completion. The marker writes only its own tiny files. It is not a backup, billing system or exactly-once transaction example. Use a staging host; do not replace an existing production schedule with this fixture.
A calendar schedule suits a particular hour. A completion-relative interval suits polling or maintenance that should rest after each run. A machine-local timer does not coordinate a cluster; use an application queue or distributed coordinator if every missed business event must be processed.
Set the public example values before copying
Open the example-values form, choose a new job name and a UTC hour from 0 to 23, then apply the values. The delay spreads starts by up to 300 seconds; it is not a deadline guarantee. The job name also chooses the unit files, script and managed state directory, so never reuse an existing application name.
Values stay in this tab for the selected tutorial and environment. Pending or invalid edits block copying until Apply or Reset. These fields accept public examples only: no passwords, tokens or private application arguments. The daily marker always uses the UTC date; changing the hour does not change its timezone.
LET’S BUILD
Check this distribution and its installed tools
This is a dated rolling-release path, reviewed on 2026-11-05, not a fixed ISO support promise. Use a normally maintained Arch Linux installation with systemd as PID 1 and record installed package builds. No AUR helper or separate timer package is required.
If required packages are missing, resolve them through the official repositories as part of the normal complete upgrade policy; do not refresh metadata for an isolated partial upgrade. The common administrator unit files and state-directory behavior stay the same. Any locally enabled AppArmor or other host policy still applies.
Terminal on the authorized staging host
cat /etc/os-release
cat /proc/1/comm
systemd --version
pacman -Q systemd coreutils sudo
command -v systemctl systemd-analyze journalctl timedatectl sudo sudoedit date mktemp mvPID 1 is systemd and all required tools are available; resolve a mismatch before continuing.
LET’S BUILD
Read the clock and preview the next calendar times
The schedule uses UTC explicitly, so a change to the host’s local timezone or daylight-saving rules does not shift the intended UTC hour. Inspect synchronization and ask systemd to calculate upcoming times before saving a timer. A valid calendar expression proves syntax, not that NTP, hardware wake-up or the application is healthy.
Keep the host’s established time service. Ordering after time-sync.target alone does not prove the clock is accurate; correlate the status with that service’s own health. Do not change the host timezone or jump its clock to rehearse catch-up.
Terminal on the authorized staging host
timedatectl status
date -u
systemd-analyze calendar --iterations=5 '*-*-* 3:00:00 UTC'The normalized expression and five upcoming UTC occurrences match the intended schedule; investigate an unsynchronized clock before activation.
LET’S BUILD
Reserve an unused job and open its three files
The fixture gets its own script, service and timer. Refuse an existing unit or state directory, including the private directory used by DynamicUser. The exclusive file creation also refuses symlinks and existing files; if a later reservation fails, earlier empty files can remain. Inspect those exact files and keep the timer disabled while correcting the reservation.
Paste the following three file panels into their matching editor buffers. The shared script directory may already exist; only the named files belong to this lesson. Do not change any vendor unit or another task’s files. Administrator rights are needed to install files, but the job itself runs with a temporary unprivileged identity.
Terminal on the authorized staging host
test "$(systemctl show -p LoadState --value 'academy-daily-marker.service')" = not-found || exit 1
test "$(systemctl show -p LoadState --value 'academy-daily-marker.timer')" = not-found || exit 1
sudo test ! -e '/var/lib/academy-daily-marker' || exit 1
sudo test ! -e '/var/lib/private/academy-daily-marker' || exit 1
sudo install -d -m 0755 /usr/local/libexec
sudo sh -c 'set -euC; : > "$1"; : > "$2"; : > "$3"' sh '/usr/local/libexec/academy-daily-marker.sh' '/etc/systemd/system/academy-daily-marker.service' '/etc/systemd/system/academy-daily-marker.timer' || exit 1
sudo chmod 0644 '/usr/local/libexec/academy-daily-marker.sh' '/etc/systemd/system/academy-daily-marker.service' '/etc/systemd/system/academy-daily-marker.timer'
sudoedit '/usr/local/libexec/academy-daily-marker.sh' '/etc/systemd/system/academy-daily-marker.service' '/etc/systemd/system/academy-daily-marker.timer'Three newly reserved, root-owned files are open. A collision stops this step without replacing an existing file.
Write a repeatable job with an atomic final marker
The job uses the state directory supplied by systemd, derives today’s UTC date, and returns success if that day’s marker already exists. Otherwise it writes a temporary file in the same directory and renames it into place. A normal interruption before the rename leaves no completed marker; an abrupt power loss can leave a temporary file.
This illustrates repeatability for one small local artifact. The rename does not make an external API call, database update or backup exactly once, and it is not a disk-flush durability protocol. A real job needs its own transaction, idempotency key or durable checkpoint. Start this script only through this service; a direct parallel invocation bypasses systemd’s serialization.
File contents — not a shell command
#!/bin/sh
set -eu
umask 077
: "${STATE_DIRECTORY:?Start this job through its systemd service}"
day=$(date -u +%F)
report="$STATE_DIRECTORY/$day.txt"
if test -f "$report"; then
printf 'ALREADY_DONE %s\n' "$day"
exit 0
fi
tmp=$(mktemp "$STATE_DIRECTORY/.report.XXXXXX")
trap 'rm -f -- "$tmp"' EXIT HUP INT TERM
printf 'academy daily marker for UTC day %s\n' "$day" > "$tmp"
mv -- "$tmp" "$report"
trap - EXIT HUP INT TERM
printf 'COMPLETED %s\n' "$day"