Stackvora.tech

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. Description is crucial for identification. After=network.target ensures 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 its ExecStart command 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.target ensures 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, Description is 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/1h would 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, if OnCalendar=daily is set, the job will run sometime within the first 15 minutes after midnight, or after boot if Persistent=true is 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.target ensures the timer is enabled and started when timers.target is 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=10min would 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=1h would allow systemd to run the task anytime within an hour of its scheduled time.
  • FixedRandomDelay=true: If set, RandomizedDelaySec will 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-reload imperative: After creating or modifying any unit file (.service or .timer), always run sudo 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 in ExecStart. The working directory for systemd services can be unexpected, so avoid relying on PATH unless explicitly configured.
  • Permissions: The script or executable defined in ExecStart must 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 .service unit (e.g., using Environment= or EnvironmentFile=).
  • Debugging: If a job isn't running or failing, always check systemctl status your-service.service and journalctl -u your-service.service first. The journal provides critical output and error messages.
  • Timer vs. Service Activation: Remember to enable and start the .timer unit (e.g., backup-app.timer), not just the .service unit, if you want it to run on a schedule. Enabling the timer will ensure the service is ready to be triggered.

Back to the knowledge base · Ask the AI assistant