GICT knowledge base
How to Schedule Tasks with systemd Timers
Published
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. InspectResultandExecMainStatuson 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.timeralso 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
Plan a Safe Redis-to-Valkey Migration
Plan a Redis-to-Valkey migration: check version compatibility, rehearse recovery, stop writers for cutover and protect against lost writes during rollback.
Linux AdministrationHow to Restrict an SSH Key to One Command
Restrict an SSH key to one command with authorized_keys options. Protect the command and check that unwanted shell and forwarding access are denied.
Linux AdministrationHow to Restrict a Linux Service with systemd Security Settings
Restrict a Linux service with systemd drop-ins, a dedicated account, read-only paths, private temporary storage, and controlled writable directories.
Linux AdministrationHow to Track Configuration File Changes in Linux with auditd
Track configuration edits on Ubuntu with persistent auditd rules, identify login accounts and processes, search events, and remove rules safely.
