Stackvora.tech

Linux systemd timers: the modern replacement for cron

For decades, cron has been the default utility for scheduling tasks on Linux and Unix-like systems. While effective for simple periodic execution, cron…

For decades, cron has been the default utility for scheduling tasks on Linux and Unix-like systems. While effective for simple periodic execution, cron presents several limitations, particularly in modern, dependency-aware, and log-centric environments. Systemd timers offer a robust, integrated alternative that addresses these shortcomings, providing enhanced logging, dependency management, and more flexible scheduling options.

This article details how to migrate from traditional cron jobs to systemd timers, explaining their architecture, configuration, and key advantages. We will explore practical examples, advanced scheduling features, and best practices for managing scheduled tasks with systemd.

The Systemd Timer Architecture: Service and Timer Units

A fundamental concept behind systemd timers is the separation of the scheduled event from the action it triggers. Unlike a single crontab entry, a systemd timer requires two unit files: a service unit and a timer unit. This two-file pattern enhances modularity and clarity.

The Service Unit (.service)

The service unit defines the task to be executed. It's essentially a standard systemd service file, often of Type=oneshot for tasks that run to completion. Key directives include:

  • Description=: A human-readable description of the task.
  • After=: Defines dependencies. For instance, After=network-online.target ensures the network is up before the task runs, crucial for backup or network-reliant scripts.
  • User=: Specifies the user account under which the command will execute. This is vital for security and permission management.
  • ExecStart=: The command or script to be executed. Use absolute paths for commands.
  • StandardOutput=journal and StandardError=journal: Directs all standard output and error to the systemd journal, centralizing logs. This replaces the need for redirecting output to files or emailing it, as is common with cron.

The Timer Unit (.timer)

The timer unit defines when and how often the associated service unit should be activated. It typically has the same base name as its corresponding service unit (e.g., nightly-backup.timer activates nightly-backup.service). Key directives within the [Timer] section include:

  • OnCalendar=: Schedules tasks based on calendar time, similar to cron's "minute hour day-of-month month day-of-week" format but more expressive. Examples: daily, weekly, *-*-* 02:00:00 (every day at 2 AM), Mon,Fri *-*-* 10:00:00.
  • OnBootSec=: Schedules a task to run a specified duration after system boot. E.g., 15min.
  • OnUnitActiveSec=: Schedules a task to run a specified duration after the associated service unit last completed execution. This creates a recurring interval based on completion, not just start time. E.g., 1h.
  • Persistent=true: Crucial for reliability. If the system is off or the timer is inactive when a scheduled event occurs, the service will be triggered immediately upon the next system boot or timer activation. This addresses a major cron limitation regarding missed jobs during downtime.
  • RandomizedDelaySec=: Adds a random delay to the scheduled execution time, up to the specified duration. This is invaluable in large fleets to prevent all machines from executing a task simultaneously (the "thundering herd" problem), reducing load on shared resources like backup servers or package repositories.
  • Unit=: Explicitly specifies the unit to activate. If omitted, systemd assumes a service unit with the same name (e.g., foo.timer activates foo.service).

The [Install] section in the timer unit (e.g., WantedBy=timers.target) enables the timer to start automatically at boot when enabled.

Practical Example: Daily Database Backup

Let's set up a daily database backup that runs every night at 2:00 AM, ensuring the network is available and logging output centrally.

1. Create the Service Unit File

File: /etc/systemd/system/nightly-backup.service

[Unit]
Description=Nightly database backup
Documentation=man:systemd.service(5)
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=backupuser
WorkingDirectory=/var/backups/db
ExecStart=/usr/local/bin/backup-db.sh
StandardOutput=journal
StandardError=journal
# Configure resource limits for the backup process
CPUSchedulingPolicy=idle
IOReadBandwidthMax=/ 10M
IORateWriteMax=/ 20M
TimeoutStartSec=5min
TimeoutStopSec=30s
# Set a memory limit, e.g., 512MB
MemoryMax=512M

In this example, backupuser should be a non-privileged user specifically created for backup tasks. The WorkingDirectory ensures the script runs from a predictable location. Resource limits (CPUSchedulingPolicy, IO*BandwidthMax, MemoryMax) are excellent additions for preventing scheduled tasks from monopolizing system resources, a feature completely absent from cron.

2. Create the Timer Unit File

File: /etc/systemd/system/nightly-backup.timer

[Unit]
Description=Run nightly database backup every day at 02:00
Documentation=man:systemd.timer(5)

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target

RandomizedDelaySec=300 means the actual execution will occur between 02:00:00 and 02:05:00, reducing the chance of simultaneous execution across many servers. Persistent=true ensures if the system is off at 02:00, the backup runs shortly after boot.

3. Reload, Enable, and Start

After creating or modifying unit files, systemd needs to reload its configuration:

sudo systemctl daemon-reload

Then, enable the timer to start at boot and start it immediately:

sudo systemctl enable --now nightly-backup.timer

The --now flag starts the timer without waiting for the next reboot.

4. Verification and Monitoring

To check the status of your timer:

systemctl status nightly-backup.timer

To see all active and inactive timers, including their next activation times:

systemctl list-timers --all

To view logs for the service execution, thanks to StandardOutput=journal:

journalctl -u nightly-backup.service --since "yesterday"
journalctl -u nightly-backup.service -f # Follow live output

This provides a complete history of executions, including standard output and errors, with timestamps and system context. No more digging through emailed cron output or log files manually redirected by a shell script.

Advanced Scheduling with Systemd Timers

Monotonic Timers

Monotonic timers are useful for tasks that need to run relative to system events or the previous run, rather than absolute calendar times. They are not affected by changes in system time, only by the passage of real time.

  • OnBootSec=: Defines a schedule relative to system boot time.
    [Timer]
    OnBootSec=15min
    Unit=my-post-boot-task.service
    This timer activates my-post-boot-task.service 15 minutes after the system finishes booting.
  • OnUnitActiveSec=: Defines a schedule relative to the last activation of the associated service.
    [Timer]
    OnUnitActiveSec=1h
    Persistent=true
    Unit=my-periodic-cleanup.service
    This will run my-periodic-cleanup.service one hour after it last completed. If Persistent=true, and the system was off, it runs an hour after boot, then an hour after subsequent runs.

Combining Calendar and Monotonic Timers

You can specify multiple OnCalendar, OnBootSec, or OnUnitActiveSec directives within a single [Timer] section. The service will be activated when any of the specified conditions are met. For example, a task that runs hourly, but also immediately on boot:

[Timer]
OnBootSec=30s
OnUnitActiveSec=1h
Unit=hourly-check.service

Refined Calendar Scheduling

The OnCalendar= directive is highly flexible. For example:

  • Mon..Fri 09:00:00: Every weekday at 9 AM.
  • *-*-1..7 04:00:00: First 7 days of every month at 4 AM.
  • *-*-* 03:00/12: Every day at 3 AM and 3 PM (every 12 hours starting at 3 AM).
  • hourly: Every hour on the hour.
  • weekly: Once a week, typically on Monday at 00:00.
  • yearly: Once a year, typically on January 1st at 00:00.

The full syntax is described in man systemd.time.

Advantages of Systemd Timers over Cron

The stub outlined three key advantages: real logs, on-boot catch-up, and dependency-aware execution. Let's expand on these and add more.

  1. Integrated Logging (Journald): All output (stdout/stderr) is captured by the systemd journal. This provides a centralized, queryable, and persistent log stream for every execution, accessible via journalctl -u your-service.service. Cron often requires manual output redirection and separate log rotation.
  2. On-Boot Catch-Up (Persistent=true): If the system is offline or the timer is inactive during a scheduled event, the job is triggered immediately upon the next boot or timer activation. Cron jobs simply miss their scheduled runs in such scenarios.
  3. Dependency Management: Service units can leverage systemd's robust dependency system (After=, Wants=, Requires=). This ensures tasks only run when necessary resources (like network, specific mounts, or other services) are available, preventing failures due to missing prerequisites. Cron offers no such built-in dependency management.
  4. Resource Control: Systemd service units allow granular control over resource usage (CPU, memory, I/O bandwidth, nice levels) using directives like CPUSchedulingPolicy, MemoryMax, IOReadBandwidthMax, etc. This is crucial for preventing scheduled tasks from degrading system performance, a capability entirely absent in cron.
  5. Randomized Delay (RandomizedDelaySec=): Prevents "thundering herd" scenarios where many servers simultaneously execute the same scheduled task, leading to resource contention. This is invaluable in large-scale deployments.
  6. Monotonic Timers: Offers flexible scheduling relative to system events (boot, last run) which are immune to wall-clock changes. Cron relies solely on absolute calendar time.
  7. Unit Status and Control: Timers and their associated services are systemd units, meaning their status, whether they are active, failed, or pending, can be easily queried with systemctl status and controlled with systemctl stop/start. This provides a consistent management interface.
  8. Environmental Consistency: Service units execute in a clean, predictable environment, which can be further customized (e.g., Environment=, UMask=). Cron's environment can be notoriously inconsistent and vary between user crontabs and system crontabs.

Common Pitfalls and Troubleshooting

  • Absolute Paths: Always use absolute paths for executables in ExecStart=. The PATH environment variable inside a systemd service might not be what you expect.
  • Permissions: Ensure the user specified by User= has the necessary permissions to execute the script and access any required files or directories.
  • Environment Variables: If your script relies on specific environment variables (e.g., for database connections), define them explicitly in the service unit using Environment="VAR=value" or EnvironmentFile=/path/to/env.conf.
  • Missing daemon-reload: After creating or modifying unit files in /etc/systemd/system/, always run sudo systemctl daemon-reload to make systemd aware of the changes.
  • Forgetting Persistent=true: Without this, missed runs (due to system downtime) will not be caught up on boot.
  • Debugging with journalctl: If a service fails, journalctl -u your-service.service --since "1 hour ago" is your first stop. Look for error messages, script output, and process exit codes.
  • After= vs. Wants=: After= ensures ordering (this unit starts after the specified unit). Wants= expresses a weaker dependency where the unit will attempt to start the specified unit, but won't fail if it doesn't. For critical dependencies like network, you might use both or Requires= for stricter enforcement.
  • Confusing Monotonic Timers: OnBootSec runs after boot. OnUnitActiveSec runs after the previous successful completion of the service. Understand the difference for precise scheduling.

Back to the knowledge base · Ask the AI assistant