Automation

Systemd Timers That Can Be Repeated

Separate scheduling from work identity, define overlap behavior and make a missed activation safe to retry.

On this page
  1. Define the work identity
  2. Keep activation and execution separate
  3. Give the service a narrow environment
  4. Make completion inspectable
  5. Exercise the retry boundary

A schedule describes when work should be considered. It does not describe whether that work is safe to repeat. An hourly housekeeping task can run late, be interrupted, or be invoked manually during investigation. The service should behave sensibly in all of those cases before a timer is allowed to activate it automatically.

Consider a task that creates one daily summary from local input files. If every activation appends another summary, a retry duplicates output. If the task silently skips an existing filename, a partial result may be mistaken for a completed result. The useful unit of design is the work interval and its state transition, rather than the wall-clock activation alone.

Define the work identity

Give each summary a stable identity such as the date, input manifest and rule version. A repeated attempt with identical input should produce the same completed result or recognize that result as already committed. A changed input manifest should be an explicit correction, not an accidental reuse of the old identity.

Write temporary output outside the published result, verify it, and then commit it using a mechanism appropriate to the storage. Keep a record distinguishing started, completed and failed attempts. A lock file by itself does not establish completion; it only coordinates access while a process is alive, depending on the locking mechanism used.

Decide what happens if the input for a date is not ready. The task can report a deferred interval and leave it eligible for a later attempt. Marking every activation successful while silently ignoring missing input hides the very condition an operator needs to notice.

Keep activation and execution separate

This illustrative timer activates a matching service roughly hourly. It is not installed by this publication:

[Unit]
Description=Consider pending summary intervals

[Timer]
OnCalendar=hourly
Persistent=true
RandomizedDelaySec=2min

[Install]
WantedBy=timers.target

For calendar timers, Persistent can trigger an activation after downtime when an activation was missed. It does not replay every missed interval as a separate service invocation. The program must enumerate any pending work itself if catching up multiple intervals is part of the requirement.

A timer does not restart its target service while that service is already active. That helps prevent overlap for this one unit, but does not coordinate a separate manual program invocation or a second host. Define a work-level lease or transaction boundary when those paths can also process the same interval.

Give the service a narrow environment

An example service for a separately installed program is:

[Unit]
Description=Build pending local summaries

[Service]
Type=oneshot
User=summary-worker
ExecStart=/usr/local/libexec/build-summaries
StateDirectory=summary-worker
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
TimeoutStartSec=5min

The user and executable must exist before this unit is enabled. The task should store its required persistent state in the managed state directory. Permissions and sandbox settings still need to be checked against the actual program; these lines are not a substitute for identifying every required read and write path.

Five minutes is an example execution bound, not a universal duration. Choose a bound that prevents a stuck task from blocking future work indefinitely while allowing a normal interval to complete. On interruption, unpublished temporary output must not be interpreted as a committed summary.

Make completion inspectable

Log the work interval, attempt identity, input count and outcome. Avoid placing secrets or raw customer records in the journal. A message saying finished is less useful than one stating which interval was committed and which intervals remain deferred.

Distinguish a task that had nothing to do from a task that could not discover its inputs. Both may produce no summary, but they are different operational states. Expose that distinction in the result record even if the activation exits successfully for an intentionally deferred interval.

Exercise the retry boundary

In an isolated directory, interrupt the program after writing temporary output. Run it again against the same input and verify that only one completed result appears. Then interrupt immediately after committing the result but before writing the final log message. The next attempt should recognize the committed work rather than duplicate it.

Test two concurrent attempts if the program can be invoked outside its timer unit. One should obtain the work boundary while the other reports that the interval is already being handled. A test that invokes only the timer cannot establish coordination for an entirely different execution path.

Finally, stop the timer for several intended intervals, then reactivate it in a disposable environment. Inspect which work the program actually discovers. The useful evidence is a list of completed and pending intervals, not simply the observation that systemd launched a process.

The timer can now remain simple because the program owns the difficult semantics: identity, retry, interruption and completion. Consult the upstream timer documentation and service documentation for the precise behavior supported by the installed systemd version.