Setting Up Reliable Task Runners with systemd Timers on RHEL
Scheduled jobs are a routine part of Linux administration. Backups, log rotation, report generation, cache cleanup, certificate checks, and monitoring tasks all need to run at predictable times. On RHEL, systemd timers provide a native way to schedule these jobs without relying exclusively on cron.
A timer activates a systemd service, while the service defines the command or script to execute. This separation makes each task easier to inspect, secure, log, and control. It also fits naturally into RHEL 8 and RHEL 9 administration, where systemd manages most system processes.
The examples below use a service account, explicit paths, and journal logging. Time zones deserve attention in Australia: a server in Sydney or Melbourne may switch between AEST and AEDT, while Perth remains on AWST. A job scheduled for “9 am” should be configured with that operational reality in mind.
Why Use A systemd Timer
Cron remains useful, but systemd timers offer better visibility and dependency handling. Administrators can check whether a timer is active, see its next scheduled run, review the last result, and connect it to targets such as network-online.target.
A timer can use a calendar schedule, such as every Sunday at 2:00 am, or a monotonic schedule, such as five minutes after boot and every hour thereafter. Persistent timers can also catch up after a machine was powered off, which is valuable for desktops, lab servers, and small office systems that may be shut down overnight.
Create The Task Script
Create a dedicated directory and script rather than placing a long command directly in the unit file:
sudo install -d -m 0750 -o root -g root /usr/local/libexec
sudo vi /usr/local/libexec/report-task.sh
Example content:
#!/bin/bash
set -euo pipefail
/usr/bin/find /var/tmp/reports -type f -mtime +14 -delete
/usr/bin/logger -t report-task "old reports removed"
Make it executable and test it manually:
sudo chmod 0750 /usr/local/libexec/report-task.sh
sudo /usr/local/libexec/report-task.sh
Use absolute command paths because systemd does not load an interactive shell profile. If the task needs environment variables, define them in the unit or use an environment file rather than assuming that .bashrc will be read.
Define The Service Unit
Create /etc/systemd/system/report-task.service:
[Unit]
Description=Clean up generated reports
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/usr/local/libexec/report-task.sh
User=root
A oneshot service runs the command and exits. For a task that should run as an unprivileged account, set User=reporter and ensure that the account has access only to the required files. Additional controls such as NoNewPrivileges=yes, PrivateTmp=yes, and ProtectSystem=full can reduce exposure, although each restriction should be tested against the script’s needs.
Load the new unit and run it directly:
sudo systemctl daemon-reload
sudo systemctl start report-task.service
sudo systemctl status report-task.service
The service does not need to be enabled because the timer will activate it. Keeping execution and scheduling separate also makes testing straightforward.
Add A Calendar Schedule
Create /etc/systemd/system/report-task.timer:
[Unit]
Description=Run report cleanup weekly
[Timer]
OnCalendar=Sun *-*-* 02:00:00
Persistent=true
Unit=report-task.service
[Install]
WantedBy=timers.target
Enable and start the timer:
sudo systemctl daemon-reload
sudo systemctl enable --now report-task.timer
systemctl list-timers --all
Persistent=true records the last activation and starts the service after the next boot if a scheduled run was missed. That behaviour is useful for maintenance jobs, but it may be unsuitable for tasks that must never run late. For those, remove the setting or build an explicit date check into the script.
Check Time Zones And Dependencies
Inspect the server’s time zone before relying on a calendar expression:
timedatectl
sudo timedatectl set-timezone Australia/Sydney
Use systemd-analyze calendar "Sun *-*-* 02:00:00" to verify how systemd interprets a schedule. For a Melbourne production host, daylight-saving changes can move the apparent local execution time unless the team agrees on a consistent policy. A Perth-based operation will have different expectations, particularly when coordinating with east-coast offices.
If a task needs DNS, an API, or a mounted remote filesystem, add suitable ordering and dependency settings. After=network-online.target controls ordering, while Wants=network-online.target requests that target be started. Neither guarantees that a remote endpoint is healthy, so the script should include retries and sensible timeouts. Older field systems that use mobile connectivity may still encounter tools such as the Sakis3G utility during network-dependent automation.
Monitor Failures And Resource Use
Read service output from the journal:
sudo journalctl -u report-task.service
sudo journalctl -u report-task.service --since today
systemctl show report-task.timer -p LastTriggerUSec -p NextElapseUSecRealtime
For recurring work, direct output to the journal with StandardOutput=journal and StandardError=journal, or configure a controlled log destination. Avoid silently discarding errors. If the job sends email or creates files, check permissions, disk space, and SELinux denials with ausearch -m avc -ts recent.
A timer can be healthy while its service fails every time. Check both units and inspect exit codes. When a task appears to consume excessive memory, compare its behaviour with this guide to memory diagnostics, using free, vmstat, and journal timestamps to correlate resource pressure with the scheduled run.
A sound systemd timer has a small script, a clearly scoped service, a verified schedule, and logs that explain every failure. Treat the timer as an operational unit rather than a hidden cron entry, and routine automation becomes easier to audit across RHEL servers from Sydney to Perth.