Set Up a Timer Instead of Cron
For Linux system administrators and developers, cron has long been the de-facto standard for scheduling tasks. However, modern Linux distributions widely…
For Linux system administrators and developers, cron has long been the de-facto standard for scheduling tasks. However, modern Linux distributions widely adopt systemd, which provides a more integrated and robust alternative for task scheduling: systemd timers. Systemd timers offer several advantages over traditional cron jobs, including better integration with systemd's service management, dependency handling, logging, and resource control. This article details the process of migrating from cron to systemd timers, covering the creation of service and timer units, their configuration, and essential management commands.
Understanding systemd Timer Units
Systemd timers function by coupling two unit types: a service unit that defines the task to be executed, and a timer unit that specifies when and how often that task should run. This separation of concerns enhances modularity and simplifies management compared to cron's monolithic crontab entries.
Service Unit (.service)
The service unit describes the actual work. It can be a simple script execution, a more complex application, or anything systemd can run as a service. For scheduled tasks, a Type=oneshot service is typically used, as it executes its command and then exits.
Consider a hypothetical backup script located at /usr/local/bin/backup-app.sh. We'll create a service unit for it:
# /etc/systemd/system/backup-app.service
[Unit]
Description=Application Data Backup Service
Documentation=man:systemd.service(5)
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-app.sh --full-backup
RemainAfterExit=no
StandardOutput=journal
StandardError=journal
# User and Group can be specified for security context
# User=backupuser
# Group=backupgroup
[Install]
WantedBy=timers.target
[Unit]: Contains generic information about the unit.Descriptionis crucial for identification.After=network.targetensures the network is up before the backup script attempts to run, which is often necessary for remote backups.[Service]: Defines the service-specific configuration.Type=oneshot: Specifies that the service will run a command and then exit. It's designed for tasks that complete and do not stay running as daemons.ExecStart: The command to execute. Include the full path to the script or binary.RemainAfterExit=no: The service will be considered inactive after itsExecStartcommand finishes.StandardOutput=journal,StandardError=journal: Directs all standard output and error to the systemd journal, making logging and debugging significantly easier than parsing mail from cron.User/Group: (Optional but recommended) Run the service under a specific, unprivileged user and group for security.
[Install]: Contains installation information.WantedBy=timers.targetensures that when the timer is enabled, this service is also implicitly considered part of the timers ecosystem, though the timer unit explicitly triggers it.
Timer Unit (.timer)
The timer unit defines the schedule. It is named identically to its corresponding service unit (e.g., backup-app.timer for backup-app.service).
# /etc/systemd/system/backup-app.timer
[Unit]
Description=Run Application Data Backup Service daily
Documentation=man:systemd.timer(5)
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=15min
# WakeSystem=true # Use if the system might be suspended or hibernated
[Install]
WantedBy=timers.target
[Unit]: Similar to the service unit,Descriptionis key.[Timer]: Defines the scheduling parameters.OnCalendar=daily: This is a powerful and flexible way to specify recurrence.daily: Runs once a day, typically at midnight or the first boot after midnight.weekly,monthly,yearly: Self-explanatory.minutely,hourly: For very frequent tasks.- Specific times:
*-*-* 03:00:00(daily at 3 AM),Mon *-*-* 10:00:00(every Monday at 10 AM). - Relative times:
hourly,minutely. - Intervals:
OnUnitActiveSec=24h(run 24 hours after the service last activated),OnBootSec=10min(run 10 minutes after boot). - For cron-like expressions,
OnCalendar=Mon..Fri *-*-* 09:00/1hwould run hourly between 9 AM and 5 PM on weekdays.
Persistent=true: If the system is powered off when the timer is supposed to run, the service will be triggered shortly after the system boots again. This prevents missed runs due to downtime.RandomizedDelaySec=15min: Adds a random delay (up to 15 minutes) to the execution time. This is invaluable in large deployments to prevent all servers from hitting a shared resource (like a backup server) simultaneously at a precise time. For example, ifOnCalendar=dailyis set, the job will run sometime within the first 15 minutes after midnight, or after boot ifPersistent=trueis also set.WakeSystem=true: (Optional) If the system supports wake-on-alarm (RTC), this can wake the system from suspend or hibernation to execute the task. Requires appropriate hardware and kernel support.
[Install]:WantedBy=timers.targetensures the timer is enabled and started whentimers.targetis reached during boot.
Enabling and Managing Timers
Once both the .service and .timer files are created and placed in /etc/systemd/system/, reload the systemd daemon to recognize the new units:
sudo systemctl daemon-reload
Then, enable and start your timer. Enabling a timer unit automatically enables its associated service unit implicitly. The --now flag starts it immediately:
sudo systemctl enable --now backup-app.timer
To check the status of your timer and its last/next run times:
systemctl list-timers
# Example output:
# NEXT LEFT LAST PASSED UNIT ACTIVATES
# Wed 2023-10-25 03:00:00 CEST 10h left Tue 2023-10-24 03:00:01 CEST 13h ago backup-app.timer backup-app.service
# Mon 2023-10-23 22:45:00 CEST 2 days left Mon 2023-10-23 22:45:01 CEST 2 days ago apt-daily.timer apt-daily.service
# ...
You can also check the status of the service unit to see if it ran successfully or if there were any issues:
systemctl status backup-app.service
# Example output:
# ● backup-app.service - Application Data Backup Service
# Loaded: loaded (/etc/systemd/system/backup-app.service; enabled; vendor preset: enabled)
# Active: inactive (dead) since Wed 2023-10-25 03:00:01 CEST; 10h ago
# Docs: man:systemd.service(5)
# Process: 12345 ExecStart=/usr/local/bin/backup-app.sh --full-backup (code=0, status=0/SUCCESS)
# Main PID: 12345 (code=exited, status=0/SUCCESS)
# CPU: 5ms
#
# Oct 25 03:00:01 server.example.com systemd[1]: Starting Application Data Backup Service...
# Oct 25 03:00:01 server.example.com systemd[1]: backup-app.service: Deactivated successfully.
# Oct 25 03:00:01 server.example.com systemd[1]: Finished Application Data Backup Service.
To view logs for the service execution (including stdout and stderr redirected to the journal):
journalctl -u backup-app.service
# To view only the most recent execution:
journalctl -u backup-app.service --since "1 hour ago"
# Or for a specific boot:
journalctl -u backup-app.service -b
To manually trigger the service for testing purposes without waiting for the timer:
sudo systemctl start backup-app.service
To stop and disable the timer:
sudo systemctl stop backup-app.timer
sudo systemctl disable backup-app.timer
This will also implicitly stop and disable the associated service from being triggered by the timer, though the service unit itself remains on the system.
Advanced Timer Configuration
Systemd timers offer more granular control than cron, such as:
OnUnitActiveSec=,OnUnitInactiveSec=: Schedule a timer relative to when the associated service last became active or inactive. E.g.,OnUnitInactiveSec=10minwould trigger 10 minutes after the service finishes.OnStartupSec=: Schedule a timer relative to when the system finished booting.OnBootSec=: Schedule a timer relative to the kernel boot time.AccuracySec=: Specifies how accurate the timer needs to be. The default is 1 minute (1min). If a task doesn't need to run at the precise second, increasing this value can help systemd optimize resource usage by grouping activations. E.g.,AccuracySec=1hwould allow systemd to run the task anytime within an hour of its scheduled time.FixedRandomDelay=true: If set,RandomizedDelaySecwill generate a delay that is fixed for the boot-up cycle or until the timer is explicitly reset. Useful for maintaining a consistent, but staggered, schedule across reboots.
Comparison with Cron
Here's a table summarizing key differences:
| Feature | Cron | systemd Timers |
|---|---|---|
| Scheduling Syntax | * * * * * command (minute hour day-of-month month day-of-week) |
OnCalendar= (e.g., daily, *-*-* 03:00:00), OnUnitActiveSec=, OnBootSec= |
| Job Definition | Inline in crontab, often a complex single line |
Dedicated .service unit, promoting modularity |
| Logging | Mail to user (/var/spool/mail), manual redirection to file, difficult to parse |
Integrated with systemd journal (journalctl -u service.service), structured and searchable |
| Dependencies | None beyond environment variables | Full systemd dependency model (After=, Requires=, etc.) |
| Resource Control | Limited via nice/ionice |
CGroup-based resource limits via CPUAccounting=, MemoryLimit=, etc., in .service file |
| Missed Jobs | Not run if system is off at scheduled time | Persistent=true ensures job runs on next boot |
| Randomization | Requires external script logic | RandomizedDelaySec= built-in |
| Environment | Limited crontab environment; requires explicit path/variables |
Inherits systemd environment, easily configurable in .service |
Common Pitfalls
daemon-reloadimperative: After creating or modifying any unit file (.serviceor.timer), always runsudo systemctl daemon-reload. Systemd does not automatically detect changes.- Incorrect paths in
ExecStart: Ensure the full, absolute path to your script or binary is used inExecStart. The working directory for systemd services can be unexpected, so avoid relying onPATHunless explicitly configured. - Permissions: The script or executable defined in
ExecStartmust have execute permissions (e.g.,chmod +x /usr/local/bin/backup-app.sh). Also, ensure the user specified (or root by default) has sufficient permissions for files and directories the script interacts with. - Script execution context: Remember that scripts executed by systemd timers run in a minimal environment. They won't have the same environment variables (like
$HOME,$PATH) as an interactive shell. Define necessary variables within the script or the.serviceunit (e.g., usingEnvironment=orEnvironmentFile=). - Debugging: If a job isn't running or failing, always check
systemctl status your-service.serviceandjournalctl -u your-service.servicefirst. The journal provides critical output and error messages. - Timer vs. Service Activation: Remember to enable and start the
.timerunit (e.g.,backup-app.timer), not just the.serviceunit, if you want it to run on a schedule. Enabling the timer will ensure the service is ready to be triggered.