Context & Why We’re Talking About Systemd Timers
If you’ve managed Linux servers for any length of time, you’ve probably become very familiar with cron. For decades, it’s been the trusted tool for scheduling everything from daily backups to hourly log rotations. And it usually just works, right?
Well, mostly. While cron is undoubtedly capable, it does have a few frustrating quirks. As DevOps engineers and system administrators, these can really complicate things.
I’ve personally spent hours debugging cron jobs that failed silently because of environmental variable differences between my shell and cron‘s execution environment. Fixing these often involves adding extensive logging to the script itself, or even manually running the command to replicate cron‘s limited context. Plus, there’s no built-in dependency management; if a critical service isn’t running, your cron job could just fail without any graceful retries or notifications, unless you bake that logic into your script manually.
This is precisely where Systemd Timers become invaluable. They provide a native, powerful, and deeply integrated replacement for cron on modern Linux distributions. If your system uses systemd (as most contemporary distributions like Ubuntu, Fedora, and CentOS/RHEL 7+ do), you already have a sophisticated task scheduler ready to use. It integrates seamlessly with your system’s services and logging.
So, why switch? For me, the advantages are undeniable. Systemd Timers offer consistent, detailed logging via journalctl, letting you see precisely what happened, when, and with what output.
They also respect system dependencies, so you can configure a task to run only after a specific service is fully operational. On top of that, they’re far easier to manage and troubleshoot using standard systemctl commands. In my experience on a production Ubuntu 22.04 server with 4GB RAM, this approach significantly cut down processing time for resource-intensive automation scripts, thanks to tighter system integration and more predictable execution environments compared to traditional cron setups.
Installation (or Rather, Verification)
Here’s the good news: if you’re on a modern Linux distribution, systemd is almost certainly already installed and running. This means Systemd Timers are available to you right out of the box. You won’t need to install any separate packages, which isn’t always the case with cron (which often requires cronie or vixie-cron).
To confirm that systemd is your init system and running correctly, you can use the following command:
systemctl --version
You should see output similar to this, indicating the version of systemd running:
systemd 249 (249.11-0ubuntu3.11)
+PAM +AUDIT +SELINUX +APPARMOR +IMA +SMACK +SECCOMP +GCRYPT +GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 +JPKG +LZ4 +ZSTD +SECCOMP +PWQUALITY +P11KIT +KERNEL_USERSPACE_HEADERS +XKBCOMMON +NSSUSELINUX +RESOLVCONF +BPF_FRAMEWORK +GLIB2
This output confirms that systemd is indeed present and ready to manage your system’s services and timers.
Configuration: Crafting Your First Systemd Timer
To schedule a task using Systemd Timers, you generally need two distinct unit files. First, a service unit (.service) defines the action to perform. Second, a timer unit (.timer) specifies when and how often to trigger that action. You can think of the .service file as your executable script, and the .timer file as its corresponding crontab entry.
Creating a Service Unit
First, let’s define the task we want to automate. For this walkthrough, we’ll create a straightforward script that logs a message and simulates a backup operation. We’ll place this script in a standard, accessible location: /usr/local/bin/.
Create a file named my-backup.sh:
#!/bin/bash
LOGFILE="/var/log/my-backup.log"
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$TIMESTAMP] Starting my daily backup..." >> $LOGFILE
# Simulate a backup operation
sleep 5
echo "[$TIMESTAMP] My daily backup finished successfully!" >> $LOGFILE
exit 0
Make it executable:
sudo chmod +x /usr/local/bin/my-backup.sh
Next, we’ll create the service unit file itself. This file instructs systemd on exactly how to execute our script. We’ll store it in /etc/systemd/system/, which is the conventional directory for custom unit files.
Create /etc/systemd/system/my-backup.service:
[Unit]
Description=My daily backup service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/my-backup.sh
WorkingDirectory=/tmp
User=root
Group=root
[Install]
WantedBy=timers.target
[Unit]: This section holds general information. TheDescriptionfield is particularly helpful for identifying the service’s purpose.[Service]: This defines the core aspects of the service.Type=oneshot: This tellssystemdthat the command will run once and then exit.systemdwill wait for its completion.ExecStart: Specifies the command or script to execute.WorkingDirectory: Sets the directory from which the command will be executed.User/Group: Defines the user and group privileges under which the script will run. This is vital for correct permissions and environmental context.
[Install]: Contains installation-related directives.WantedBy=timers.targetindicates that this service is designed to be triggered by a timer.
Creating a Timer Unit
Now, we’ll define the timer unit responsible for activating our service. Like the service file, this will reside in /etc/systemd/system/ and must share the same base name as the service, but with a .timer extension.
Create /etc/systemd/system/my-backup.timer:
[Unit]
Description=Run my daily backup timer
[Timer]
OnCalendar=daily
Persistent=true
Unit=my-backup.service
[Install]
WantedBy=timers.target
[Unit]: Similar to the service unit, this section is for a descriptive name.[Timer]: This is where you configure the timing parameters.OnCalendar=daily: This critical setting specifies the schedule. Whiledailyis simple,systemdoffers highly flexible expressions:hourly: Executes every hour.*-*-* 03:00:00: Runs every day precisely at 3 AM.Mon,Fri *-*-* 18:00:00: Triggers every Monday and Friday at 6 PM.minutely: Executes every minute (use with extreme caution for resource-intensive tasks!).OnUnitActiveSec=5min: Runs 5 minutes after the associated service was last active.OnBootSec=10min: Executes 10 minutes after the system boots up.
Persistent=true: A powerful feature. Ifsystemdhappens to be offline when a scheduled run was due, the job will execute immediately upon system boot. This mimicsanacron‘s behavior for traditionalcronjobs.Unit=my-backup.service: This directive tells the timer exactly which service unit to activate when the schedule is met.
[Install]: Here,WantedBy=timers.targetensures our timer is properly integrated into the system’s timer management framework.
Enabling and Starting Your Timer
Once you’ve created or modified your unit files, it’s essential to reload the systemd daemon. This ensures it picks up all your new changes:
sudo systemctl daemon-reload
With the daemon reloaded, you can now enable and start your timer. Enabling it ensures it will activate automatically on system boot, while starting it immediately activates it for the current session.
sudo systemctl enable my-backup.timer
sudo systemctl start my-backup.timer
It’s important to note that we’re enabling and starting the .timer unit, not the .service unit directly. The timer acts as the trigger, orchestrating when the service will run.
Verification & Monitoring
One of the most compelling advantages of Systemd Timers is how straightforward it is to verify and monitor their execution.
Checking Timer Status
To inspect all active timers across your system, and to specifically check the status of your newly created timer, use the list-timers command:
systemctl list-timers
You’ll get output similar to this:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2026-03-20 00:00:00 UTC 10h left Wed 2026-03-19 00:00:00 UTC 14h ago my-backup.timer my-backup.service
2 timers listed.
This output clearly indicates when your timer is next scheduled to run (under NEXT) and its last execution time (LAST). For even more granular details on your specific timer, you can use:
systemctl status my-backup.timer
The output will confirm if it’s active and enabled:
● my-backup.timer - Run my daily backup timer
Loaded: loaded (/etc/systemd/system/my-backup.timer; enabled; vendor preset: enabled)
Active: active (waiting) since Wed 2026-03-19 14:00:00 UTC; 14h ago
Trigger: Thu 2026-03-20 00:00:00 UTC; 10h left
Triggers: ● my-backup.service
Docs: man:systemd.timer(5)
Checking Service Status and Logs
Once the timer triggers, the associated service unit springs into action. To confirm the service executed successfully and to review its output, you’ll want to inspect the service unit’s status and its journal logs.
systemctl status my-backup.service
This will show you the last execution status:
● my-backup.service - My daily backup service
Loaded: loaded (/etc/systemd/system/my-backup.service; static)
Active: inactive (dead) since Wed 2026-03-19 00:00:05 UTC; 14h ago
Main PID: 12345 (code=exited, status=0/SUCCESS)
CPU: 50ms
For detailed output, including anything your script printed to standard output or standard error, journalctl is an incredibly powerful tool:
journalctl -u my-backup.service --since "1 day ago"
This command filters the system journal to show entries exclusively from my-backup.service, going back one day. You’ll find the echo messages from our sample script here, confirming its successful execution. This centralized logging capability offers a significant advantage over sifting through scattered log files or manually configuring logging for each individual cron script.
Troubleshooting Tips
- Permissions: Always ensure your script has execute permissions (
chmod +x). Also, verify that theUserspecified in your service unit has the appropriate permissions for any files or directories the script interacts with. - Paths: For scripts and any files they access within your service unit, always use absolute paths. Remember, the execution environment for
systemdservices can be quite minimal. - Reload: Did you forget to run
sudo systemctl daemon-reloadafter making changes? If so, your updates won’t take effect. This is a very common oversight! - Timezones: By default,
OnCalendarexpressions rely on the system’s local timezone. Keep this in mind, especially in multi-server or geographically distributed environments. - Stopping/Disabling: If a timer isn’t behaving as expected, or if you need to temporarily pause it, you can use these commands:
sudo systemctl stop my-backup.timer sudo systemctl disable my-backup.timerThe
stopcommand will halt any current activity, whiledisableprevents the timer from starting automatically on subsequent boots.
While migrating from cron to Systemd Timers might initially feel like a significant change, the benefits quickly become apparent. Once you integrate with the systemd ecosystem, you’ll gain enhanced control, superior debugging capabilities, and seamless integration with the rest of your Linux system. This is a genuinely contemporary approach to task scheduling, perfectly suited for today’s server management demands.

