- Build a systemd Timer You Can Test and Trust
- Jump to a section
- What you will build
- Check the system and choose unused names
- Create a harmless Bash-compatible script
- Create the service unit
- Create the timer unit
- Check the calendar, validate the files and test the service
- Enable the timer and inspect the next run
- Clean up and decide on a real schedule
- Which systemd timer settings should you use?
- When the timer appears to work but nothing happens
- Quick knowledge check
- How to Troubleshoot systemd Timers
- Documentation and further reading
Build a systemd Timer You Can Test and Trust
You have a script that works when you run it yourself. Now you want Linux to run it on schedule—and you need a way to prove it actually ran. This tutorial builds a working systemd timer from scratch without touching real backups or production data.
What you will build
The goal is modest: a demo command prints a timestamp. A .service unit runs the command; a .timer unit starts the service every five minutes. This systemd timer is intentionally simple, so each stage is easy to inspect. You will also start the service manually, so you can verify success without waiting for a scheduled run.
Quick answer: To create a systemd timer, write an executable script, create a matching .service file and .timer file under /etc/systemd/system/, validate them, reload systemd, and use systemctl enable --now on the timer—not the service. Inspect the service journal to confirm execution.
This is a hypothetical training exercise, not a benchmark or a backup recipe. It is designed for an Ubuntu, Debian, or similar Linux server using systemd as PID 1. Use a disposable VM or a noncritical test host; you need sudo access. The commands below change local system configuration, so read the cleanup step before starting.
Check the system and choose unused names
Confirm that systemd is installed and that the unprivileged nobody account exists. Check that the three example paths are unused before creating anything. A test command is silent when it succeeds; if a path already exists, stop and use a different lab name rather than overwriting someone else’s files.
systemctl --version | head -n 1
id nobody
if [ -e /usr/local/bin/sa-timer-demo.sh ] ||
[ -e /etc/systemd/system/sa-timer-demo.service ] ||
[ -e /etc/systemd/system/sa-timer-demo.timer ]; then
printf 'Stop: a demo path already exists.\n'
else
printf 'All demo paths are available.\n'
fiThese files use the sa-timer-demo prefix so they are easy to identify. If any preflight test returns an error, inspect the conflict before proceeding. Do not run the creation commands on an important server without understanding your local unit naming.
Create a harmless Bash-compatible script
Rather than modifying logs, databases, or user files, the demo prints one line containing the time. This makes the job visible in the journal without needing a writable directory. A systemd timer can manage a much more complex script later, but the basic setup is the same.
sudo tee /usr/local/bin/sa-timer-demo.sh >/dev/null <<'EOF'
#!/bin/sh
set -eu
printf 'sa-timer-demo: executed at %s\n' "$(/usr/bin/date --iso-8601=seconds)"
EOF
sudo chmod 0755 /usr/local/bin/sa-timer-demo.sh
/usr/local/bin/sa-timer-demo.shThe final command runs the script once in your current terminal. If that test fails, fix the script first; a timer cannot repair a broken program. Its output should resemble sa-timer-demo: executed at 2026-10-16T12:15:00+00:00, but your actual time and time zone will differ.
Create the service unit
The service defines what systemd will run. For this single action, Type=oneshot is a good fit: systemd waits for the script to finish instead of expecting a long-running daemon.
sudo tee /etc/systemd/system/sa-timer-demo.service >/dev/null <<'EOF'
[Unit]
Description=Servers Academy demonstration task
[Service]
Type=oneshot
User=nobody
ExecStart=/usr/local/bin/sa-timer-demo.sh
StandardOutput=journal
StandardError=journal
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
EOF| Setting | Why this lab uses it |
|---|---|
Type=oneshot | The program runs, exits and is not meant to stay active. |
User=nobody | Runs this read-only example without root privileges. Verify the account exists first. |
ExecStart=... | Gives systemd an absolute executable path. |
StandardOutput=journal | Sends the script’s printed line to the journal. |
NoNewPrivileges=, ProtectSystem=, ProtectHome= | Constrain this read-only demonstration. A real job that writes files needs deliberately chosen permissions and paths. |
A successfully completed oneshot service will normally return to inactive (dead). That is not evidence of failure. Its exit result and journal entries tell the story. For background on unit structure, see how to create a systemd service.
Create the timer unit
Now define when the service runs. This is the systemd timer unit, separate from the service it will activate. The expression below means minute 00, 05, 10, 15 and so on, every hour. Five minutes is useful for a short lab; you would probably choose a less frequent schedule for a real maintenance job.
sudo tee /etc/systemd/system/sa-timer-demo.timer >/dev/null <<'EOF'
[Unit]
Description=Run the Servers Academy demo every five minutes
[Timer]
OnCalendar=*-*-* *:00/5:00
AccuracySec=1min
Persistent=false
Unit=sa-timer-demo.service
[Install]
WantedBy=timers.target
EOFWe do not need catch-up behavior in a disposable five-minute lab. Persistent=true can trigger a missed calendar occurrence when the timer becomes active again, but it does not queue every missed interval. This is explained in the previous Foundations lesson and the official timer manual.
Setting AccuracySec=1min allows systemd to coalesce timer events within roughly the following minute rather than promise execution on the exact second. The local system clock and time zone affect calendar calculations; review them with timedatectl if your output seems unexpected.
Check the calendar, validate the files and test the service
Before enabling the schedule, ask systemd to parse the time expression and show its next occurrences. Then verify the unit definitions, reload the manager and start the service once manually. This isolates script errors from timer errors.
systemd-analyze calendar --iterations=3 '*-*-* *:00/5:00'
sudo systemd-analyze verify /etc/systemd/system/sa-timer-demo.service /etc/systemd/system/sa-timer-demo.timer
sudo systemctl daemon-reload
sudo systemctl start sa-timer-demo.service
sudo journalctl -u sa-timer-demo.service -n 12 --no-pagerLook for the sa-timer-demo: executed at ... message in the service journal. Your timestamps will depend on the machine, and journal access may require sudo. The calendar expression was independently syntax-checked on Debian 13 with systemd 257; this does not claim that the tutorial was executed on your server.
Enable the timer and inspect the next run
Enable the timer so it starts again after boot, and start it now. The systemd timer will take care of activating its matching service when the configured time arrives.
sudo systemctl enable --now sa-timer-demo.timer
systemctl is-enabled sa-timer-demo.timer
systemctl is-active sa-timer-demo.timer
systemctl list-timers --all sa-timer-demo.timer
systemctl status sa-timer-demo.timer --no-pageris-enabled answers whether the timer is configured to start automatically; is-active tells you if it is currently running. list-timers shows the next and previous trigger times when available. None of these, by itself, proves the script finished successfully: use the service journal for that.
systemctl enable --now sa-timer-demo.service is not the correct command for this example: the service has no [Install] section, and you want the .timer to control activation. Also, a timer cannot start a second copy of an already-active target service; a long-running job can prevent a subsequent activation rather than produce overlapping copies.
Clean up and decide on a real schedule
When you have seen the scheduled run—or checked a manual run and the next planned trigger—remove the demonstration. These commands operate only on the three paths introduced in this systemd timer tutorial. Read each path before running them.
sudo systemctl disable --now sa-timer-demo.timer
sudo rm -- /etc/systemd/system/sa-timer-demo.timer
sudo rm -- /etc/systemd/system/sa-timer-demo.service
sudo rm -- /usr/local/bin/sa-timer-demo.sh
sudo systemctl daemon-reloadFor a real job, choose the required account and filesystem permissions deliberately, use absolute paths, test manually, and decide whether catch-up execution is safe. Backups, in particular, require verifying actual restore capability; seeing a timer fire is not a backup test.
Which systemd timer settings should you use?
A useful systemd timer starts with the requirements of the job, not a collection of copied directives. Do not combine settings just because they look useful.
| Need | Timer option | Decision |
|---|---|---|
| Run at a recognizable clock time | OnCalendar= | Use a calendar expression; check time zone and clock accuracy. |
| Run after the machine boots | OnBootSec= | Relative to boot time; not a daily wall-clock schedule. |
| Run again after the unit was activated | OnUnitActiveSec= | Relative to the last activation, not an exact clock boundary. |
| Recover one missed calendar occurrence | Persistent=true | Useful only when the job can tolerate catch-up behavior; not a replay queue. |
| Avoid a synchronized workload spike | RandomizedDelaySec= | Spread activations out; not needed in this small lab. |
If you already use cron, read Cron vs systemd Timers before migrating. The two tools have different operational tradeoffs; one is not universally better.
When the timer appears to work but nothing happens
| What you observe | Likely next check |
|---|---|
| Calendar command rejects the expression | Correct OnCalendar before editing the unit again. |
| Timer active, but no execution line | Check the next scheduled trigger, then the associated service and journal. |
| Service fails immediately | Check script permissions, absolute path, User= and journalctl output. |
Oneshot reads inactive (dead) | Normal after successful completion; inspect the exit result and journal. |
| Changes do not take effect | Run daemon-reload, then restart the timer if you changed its active schedule. |
For detailed log-reading techniques, see journalctl Explained. The next lesson in this mini-course will focus on diagnosing missed runs, failed units and recurring scheduling problems.
Quick knowledge check
Does enabling the .timer also mean the .service ran successfully?
No. Enabling starts the scheduling mechanism. Check the service result and journal output for evidence of execution.
Why do we start the service manually before enabling the timer?
To confirm the script and service configuration work independently of any scheduling rule.
Should a Type=oneshot service remain active after it finishes?
Usually not. Without RemainAfterExit=yes, it normally returns to inactive after completing its work.
Is a five-minute schedule appropriate for production backups?
Not automatically. Choose the interval from recovery requirements, data volume, system impact and restore-test results. This article’s schedule is for a small, disposable lab.
Documentation and further reading
- systemd.timer(5) — calendar and monotonic settings, accuracy, persistence and activation behavior.
- systemd-analyze(1) — validating unit files and parsing calendar expressions.
- systemd.service(5) — oneshot units and service lifecycle.
- systemctl(1) — enable, start, list-timers and status commands.
- What Is systemd? — background on units, targets and system startup.
- How to Create a systemd Service — service unit fundamentals.








