WIUNIX ACADEMY
Reliable systemd timers: missed runs, repeatable jobs and completion intervals
Page 2 of 4. Review the earlier steps and prerequisites before continuing.
YOUR 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
Let the oneshot return to inactive after finishing
The service waits for the script to exit. Success returns it to inactive, so the next timer activation can start it again; leave RemainAfterExit unset. systemd allocates an unprivileged dynamic user and retains its private persistent state between starts. Do not create that state directory manually or assign its recycled numeric user ID to other files.
The two-minute startup timeout bounds this tiny fixture and can terminate it on expiry. For a real maintenance program, choose a timeout from its actual work and interruption behavior. A successful service exit is still only the program’s claim; check its meaningful output too.
File contents — not a shell command
[Unit]
Description=Academy daily marker job
[Service]
Type=oneshot
DynamicUser=yes
StateDirectory=academy-daily-marker
StateDirectoryMode=0700
ExecStart=/bin/sh /usr/local/libexec/academy-daily-marker.sh
TimeoutStartSec=2min
Schedule a daily UTC start with downtime catch-up
The timer requests one daily activation at 3:00 UTC, adds up to 300 seconds of random delay, and permits a further one-second accuracy window. Scheduler load can delay execution further. Random delay spreads a fleet’s starts; it does not reserve capacity or provide an SLA.
Persistent records the timer’s last trigger on disk. When the timer returns after inactivity, one or more missed occurrences cause one catch-up activation, still subject to the random delay. It does not replay every missed day and does not record whether the job succeeded. Enabling a timer with missed time can therefore start work soon; inspect that possibility before using this pattern for a real job.
File contents — not a shell command
[Unit]
Description=Academy daily UTC schedule
[Timer]
OnCalendar=*-*-* 3:00:00 UTC
Persistent=yes
RandomizedDelaySec=300s
AccuracySec=1s
Unit=academy-daily-marker.service
[Install]
WantedBy=timers.target
LET’S BUILD
Validate the files and run the isolated job once
Check shell syntax and the installed systemd parser before loading the new files. Read every verifier message, including warnings about ignored settings. Then start only the fixture service and inspect its journal and marker directory; the timer remains disabled at this point.
A successful oneshot normally shows inactive after it finishes, Result=success and ExecMainStatus=0. The first run should log COMPLETED and create one dated text file. With DynamicUser, the public state path can be a symlink into systemd’s private state area; use authorized inspection, not ownership changes or a broad permission fix.
Terminal on the authorized staging host
sudo /bin/sh -n '/usr/local/libexec/academy-daily-marker.sh' || exit 1
sudo systemd-analyze verify '/etc/systemd/system/academy-daily-marker.service' '/etc/systemd/system/academy-daily-marker.timer' || exit 1
sudo systemctl daemon-reload || exit 1
sudo systemctl start 'academy-daily-marker.service' || exit 1
sudo systemctl show 'academy-daily-marker.service' -p ActiveState -p Result -p ExecMainStatus -p StateDirectory
sudo journalctl -u 'academy-daily-marker.service' --since '-5 min' --no-pager -n 40
sudo ls -l '/var/lib/academy-daily-marker/'The job exits successfully and its dated marker exists. A syntax pass or active timer alone is insufficient.
LET’S BUILD
Check repeatability, then enable only the timer
A second service start on the same UTC day should report ALREADY_DONE and preserve the single completed marker. After confirming that result, enable and start the timer. Enable the timer rather than the oneshot service: the schedule should pull in the service when needed.
Inspect NEXT and the effective properties. LAST or LastTriggerUSec refers to timer activation, not application completion; a manual service run does not establish a successful scheduled run. Keep the journal and the job’s own output as separate evidence. Persistent timestamps are bookkeeping, not a work queue.
Terminal on the authorized staging host
sudo systemctl start 'academy-daily-marker.service' || exit 1
sudo journalctl -u 'academy-daily-marker.service' --since '-5 min' --no-pager -n 40
sudo systemctl enable --now 'academy-daily-marker.timer' || exit 1
systemctl is-enabled --quiet 'academy-daily-marker.timer' || exit 1
systemctl is-active --quiet 'academy-daily-marker.timer' || exit 1
systemctl list-timers --all 'academy-daily-marker.timer'
systemctl show 'academy-daily-marker.timer' -p ActiveState -p SubState -p LastTriggerUSec -p NextElapseUSecRealtime -p RandomizedDelayUSec -p AccuracyUSec
The repeat start reports ALREADY_DONE. The timer is enabled and waiting with a plausible next occurrence.
Understand overlap, sleep and failed runs
If the same service is still active when a timer expires, systemd leaves that instance running rather than creating a second concurrent instance. This is local protection for this exact unit, not a lock against a second unit, a direct script run or another machine. Missed activations are not a durable backlog.
A calendar timer that becomes due while the host sleeps runs after resume; several sleep-time occurrences collapse into one activation. WakeSystem is off here, so this timer does not wake the machine. Monotonic timers normally pause their clock during suspend. These sleep semantics differ from catch-up after a timer was inactive.
Persistent does not retry a failed service until success. A later scheduled event may start it again; application retries need a bounded, deliberate policy. Avoid enabling an aggressive restart loop for a non-idempotent task. For long jobs, choose a completion-relative interval if the desired rule is a pause after finishing.
LET’S BUILD
Find why the schedule did not fire when expected
Compare the effective timer with its next occurrence, loaded source path and clock status. A disabled timer, wrong unit name, local-time expression, random delay or stale loaded configuration can explain an apparently missed start. Inspect before changing anything; do not erase the persistent timestamp to make the display look fresh.
If the next time is absent or unexpected, resolve the parser messages, any drop-ins and the actual clock. A timer shown as waiting says nothing about whether the prior job produced a valid result.
Terminal on the authorized staging host
systemctl status 'academy-daily-marker.timer' --no-pager
systemctl cat 'academy-daily-marker.timer'
systemctl show 'academy-daily-marker.timer' -p FragmentPath -p DropInPaths -p ActiveState -p LastTriggerUSec -p NextElapseUSecRealtime
systemctl list-timers --all 'academy-daily-marker.timer'
timedatectl statusThe missing or delayed start has a concrete explanation in effective configuration or clock evidence; no schedule state has been changed.