Systemd Timers as Cron Alternative: Modern Task Scheduling

Introduction

Systemd timers provide a modern, powerful alternative to traditional cron jobs for scheduling tasks on Linux systems. While cron has served Unix-like systems for decades, systemd timers offer enhanced features including dependency management, resource control, detailed logging, and integration with the systemd ecosystem. Understanding systemd timers is essential for modern system administration and DevOps practices.

This comprehensive guide explores systemd timers from basics to advanced patterns, demonstrating how to migrate from cron and implement sophisticated scheduling scenarios.

Why Choose Systemd Timers Over Cron?

Advantages of Systemd Timers

Better Logging: Integration with journald provides structured, queryable logs Dependency Management: Start tasks only when specific services are available Resource Control: Use cgroups for CPU, memory, and I/O limits Event-Based Triggers: Trigger on boot, service start, or file changes Calendar Expressions: More flexible scheduling syntax Failure Handling: Automatic retry with configurable backoff Environment Management: Better control over execution environment Monitoring: Native status checking and integration with monitoring tools

When to Use Systemd Timers

  • Modern Linux distributions (Ubuntu 16.04+, CentOS 7+, Debian 8+)
  • Tasks requiring resource limits
  • Complex dependency chains
  • Need for detailed logging and monitoring
  • Integration with other systemd services

Prerequisites

  • Linux system with systemd (check: systemctl --version)
  • Root or sudo access
  • Basic understanding of systemd services
  • Familiarity with cron concepts

Systemd Timer Basics

Timer Components

Every systemd timer consists of two files:

  1. Service Unit (.service): Defines what to execute
  2. Timer Unit (.timer): Defines when to execute

File Locations

# System timers
/etc/systemd/system/

# User timers
~/.config/systemd/user/

# System defaults
/lib/systemd/system/

Creating Your First Timer

Example: Daily Backup Timer

Step 1: Create the Service Unit

# /etc/systemd/system/backup.service
[Unit]
Description=Daily Backup Service
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
Group=backup
StandardOutput=journal
StandardError=journal

# Resource limits
CPUQuota=50%
MemoryLimit=512M

Step 2: Create the Timer Unit

# /etc/systemd/system/backup.timer
[Unit]
Description=Daily Backup Timer
Requires=backup.service

[Timer]
# Run daily at 2:00 AM
OnCalendar=daily
OnCalendar=*-*-* 02:00:00
Persistent=true

# Run immediately if missed
Persistent=true

[Install]
WantedBy=timers.target

Step 3: Enable and Start

# Reload systemd
sudo systemctl daemon-reload

# Enable timer (start on boot)
sudo systemctl enable backup.timer

# Start timer now
sudo systemctl start backup.timer

# Check status
sudo systemctl status backup.timer

# List all timers
systemctl list-timers

# View logs
journalctl -u backup.service

Calendar Expression Syntax

Time Specifications

# OnCalendar examples

# Every minute
OnCalendar=*-*-* *:*:00

# Every 5 minutes
OnCalendar=*-*-* *:0/5:00

# Every hour
OnCalendar=hourly
OnCalendar=*-*-* *:00:00

# Every day at midnight
OnCalendar=daily
OnCalendar=*-*-* 00:00:00

# Every day at specific time
OnCalendar=*-*-* 03:30:00

# Every Monday at 9 AM
OnCalendar=Mon *-*-* 09:00:00

# Weekdays at 6 PM
OnCalendar=Mon..Fri *-*-* 18:00:00

# First day of month
OnCalendar=*-*-01 00:00:00

# Every 15 minutes
OnCalendar=*:0/15

# Multiple times
OnCalendar=*-*-* 06:00:00
OnCalendar=*-*-* 18:00:00

# Business hours (9-5, Mon-Fri)
OnCalendar=Mon..Fri 09..17:00:00

Test Calendar Expressions

# Test calendar expression
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"

# Output shows:
#  Original form: Mon..Fri *-*-* 09:00:00
#  Normalized form: Mon..Fri *-*-* 09:00:00
#  Next elapse: Mon 2024-01-15 09:00:00
#  From now: 3 days left

Advanced Timer Configurations

1. System Health Check Timer

# /etc/systemd/system/health-check.service
[Unit]
Description=System Health Check
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/health-check.sh
User=monitoring
StandardOutput=journal
StandardError=journal

# Timeout after 5 minutes
TimeoutStartSec=300

# Restart on failure
Restart=on-failure
RestartSec=60
# /etc/systemd/system/health-check.timer
[Unit]
Description=Run Health Check Every 5 Minutes

[Timer]
# Every 5 minutes
OnBootSec=5min
OnUnitActiveSec=5min

# Run immediately on boot
OnBootSec=1min

[Install]
WantedBy=timers.target

2. Database Backup with Dependencies

# /etc/systemd/system/db-backup.service
[Unit]
Description=Database Backup
Requires=postgresql.service
After=postgresql.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-database.sh

# Run as specific user
User=postgres
Group=postgres

# Set environment
Environment="PGDATA=/var/lib/postgresql/data"
EnvironmentFile=/etc/db-backup.conf

# Resource limits
Nice=19
IOSchedulingClass=2
IOSchedulingPriority=7

# Logging
StandardOutput=journal+console
StandardError=journal+console
# /etc/systemd/system/db-backup.timer
[Unit]
Description=Daily Database Backup at 2 AM
Requires=db-backup.service

[Timer]
# Daily at 2 AM
OnCalendar=*-*-* 02:00:00

# Catch up if missed
Persistent=true

# Randomize by up to 30 minutes
RandomizedDelaySec=1800

[Install]
WantedBy=timers.target

3. Log Rotation Timer

# /etc/systemd/system/log-rotation.service
[Unit]
Description=Rotate Application Logs

[Service]
Type=oneshot
ExecStart=/usr/local/bin/rotate-logs.sh

# Run with restricted privileges
User=logger
Group=logger
PrivateTmp=yes
ProtectSystem=strict
ReadWritePaths=/var/log/app

StandardOutput=journal
# /etc/systemd/system/log-rotation.timer
[Unit]
Description=Rotate Logs Daily at Midnight

[Timer]
# Daily at midnight
OnCalendar=daily

# Also run weekly even if not triggered
OnCalendar=weekly

Persistent=true

[Install]
WantedBy=timers.target

4. Cleanup Timer with Multiple Schedules

# /etc/systemd/system/cleanup.service
[Unit]
Description=Cleanup Temporary Files

[Service]
Type=oneshot
ExecStart=/usr/local/bin/cleanup-temp.sh
ExecStart=/usr/local/bin/cleanup-old-logs.sh
ExecStart=/usr/local/bin/cleanup-cache.sh

User=root
StandardOutput=journal
# /etc/systemd/system/cleanup.timer
[Unit]
Description=Regular System Cleanup

[Timer]
# Run daily at 3 AM
OnCalendar=*-*-* 03:00:00

# Also run weekly on Sunday
OnCalendar=Sun *-*-* 01:00:00

# And monthly on first day
OnCalendar=*-*-01 02:00:00

Persistent=true
AccuracySec=1h

[Install]
WantedBy=timers.target

Migrating from Cron to Systemd Timers

Cron to Timer Conversion

Cron ExpressionSystemd Timer Equivalent
*/5 * * * *OnUnitActiveSec=5min
0 2 * * *OnCalendar=*-*-* 02:00:00
0 0 * * 0OnCalendar=Sun *-*-* 00:00:00
0 0 1 * *OnCalendar=*-*-01 00:00:00
@rebootOnBootSec=1min
@dailyOnCalendar=daily
@hourlyOnCalendar=hourly

Conversion Script

#!/bin/bash
# cron-to-systemd.sh
# Convert cron job to systemd timer

CRON_SCHEDULE="$1"
SERVICE_NAME="$2"
COMMAND="$3"

# Create service file
cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
[Unit]
Description=${SERVICE_NAME} Service

[Service]
Type=oneshot
ExecStart=${COMMAND}
StandardOutput=journal
StandardError=journal
EOF

# Create timer file based on cron schedule
# (Simplified conversion)
TIMER_SPEC=""
case "$CRON_SCHEDULE" in
    "@hourly")
        TIMER_SPEC="OnCalendar=hourly"
        ;;
    "@daily")
        TIMER_SPEC="OnCalendar=daily"
        ;;
    "@weekly")
        TIMER_SPEC="OnCalendar=weekly"
        ;;
    *)
        echo "Manual conversion required for: $CRON_SCHEDULE"
        ;;
esac

cat > "/etc/systemd/system/${SERVICE_NAME}.timer" <<EOF
[Unit]
Description=${SERVICE_NAME} Timer

[Timer]
${TIMER_SPEC}
Persistent=true

[Install]
WantedBy=timers.target
EOF

systemctl daemon-reload
echo "Created ${SERVICE_NAME}.service and ${SERVICE_NAME}.timer"

Advanced Features

1. Event-Based Triggers

# /etc/systemd/system/process-uploads.service
[Unit]
Description=Process New Uploads

[Service]
Type=oneshot
ExecStart=/usr/local/bin/process-uploads.sh
# /etc/systemd/system/process-uploads.path
[Unit]
Description=Monitor Uploads Directory

[Path]
# Watch for new files
PathChanged=/var/uploads
# Trigger service
Unit=process-uploads.service

[Install]
WantedBy=multi-user.target

2. Resource Management

# /etc/systemd/system/heavy-task.service
[Unit]
Description=Resource-Intensive Task

[Service]
Type=oneshot
ExecStart=/usr/local/bin/heavy-task.sh

# CPU limit (50%)
CPUQuota=50%

# Memory limit
MemoryLimit=1G
MemoryMax=1G

# I/O priority (best effort, low priority)
IOSchedulingClass=2
IOSchedulingPriority=7

# Nice value
Nice=19

# Limit open files
LimitNOFILE=1024

3. Failure Handling and Retries

# /etc/systemd/system/api-sync.service
[Unit]
Description=Sync with External API
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/api-sync.sh

# Retry on failure
Restart=on-failure
RestartSec=60

# Maximum restart attempts
StartLimitBurst=3
StartLimitIntervalSec=300

# Timeout
TimeoutStartSec=180

Managing Systemd Timers

Essential Commands

# List all timers
systemctl list-timers --all

# Show timer details
systemctl status backup.timer

# Show service status
systemctl status backup.service

# View logs
journalctl -u backup.service
journalctl -u backup.service --since "1 hour ago"
journalctl -u backup.service -f  # Follow

# Start timer manually
systemctl start backup.timer

# Stop timer
systemctl stop backup.timer

# Enable on boot
systemctl enable backup.timer

# Disable
systemctl disable backup.timer

# Restart timer
systemctl restart backup.timer

# Trigger service immediately
systemctl start backup.service

# Check when timer will run next
systemctl list-timers backup.timer

# Reload configuration
systemctl daemon-reload

Monitoring and Debugging

# Check timer calendar schedule
systemd-analyze calendar "*-*-* 02:00:00"

# Verify timer configuration
systemd-analyze verify /etc/systemd/system/backup.timer

# View timer properties
systemctl show backup.timer

# Check service dependencies
systemctl list-dependencies backup.service

# Debug mode
SYSTEMD_LOG_LEVEL=debug systemctl start backup.service

User Timers

Creating User Timers

# Create user systemd directory
mkdir -p ~/.config/systemd/user

# Create service
cat > ~/.config/systemd/user/my-task.service <<EOF
[Unit]
Description=My Personal Task

[Service]
Type=oneshot
ExecStart=/home/user/bin/my-task.sh
EOF

# Create timer
cat > ~/.config/systemd/user/my-task.timer <<EOF
[Unit]
Description=My Task Timer

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target
EOF

# Reload user daemon
systemctl --user daemon-reload

# Enable and start
systemctl --user enable --now my-task.timer

# List user timers
systemctl --user list-timers

Enable Lingering

# Allow user timers to run when not logged in
sudo loginctl enable-linger $USER

# Check linger status
loginctl show-user $USER | grep Linger

Best Practices

1. Naming Conventions

# Use descriptive names
backup-database.timer
cleanup-logs.timer
check-ssl-certificates.timer

2. Documentation

[Unit]
Description=Clear description of what this timer does
Documentation=https://wiki.company.com/backup-procedure

3. Failure Notifications

# /etc/systemd/system/backup.service
[Service]
ExecStart=/usr/local/bin/backup.sh
OnFailure=failure-notification@%n.service

4. Testing

# Test service before enabling timer
systemctl start backup.service

# Check logs
journalctl -u backup.service -n 50

# Verify exit status
systemctl show -p Result backup.service

5. Security

[Service]
# Run as specific user
User=backup
Group=backup

# Sandboxing
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/backup

# Capabilities
NoNewPrivileges=yes

Troubleshooting

Common Issues

Timer not running:

# Check if enabled
systemctl is-enabled backup.timer

# Check status
systemctl status backup.timer

# View systemd logs
journalctl -xe

Service fails:

# Check service logs
journalctl -u backup.service -n 100

# Test service manually
systemctl start backup.service

# Check service file syntax
systemd-analyze verify /etc/systemd/system/backup.service

Timer runs at wrong time:

# Verify calendar expression
systemd-analyze calendar "daily"

# Check timer configuration
systemctl cat backup.timer

# List next trigger time
systemctl list-timers backup.timer

Conclusion

Systemd timers provide a modern, feature-rich alternative to cron for scheduling tasks on Linux systems. With better logging, resource management, and integration with the systemd ecosystem, timers offer significant advantages for system administration and automation.

Key takeaways:

  • Systemd timers consist of .service and .timer units
  • Calendar expressions are more flexible than cron syntax
  • Integration with journald provides better logging
  • Resource limits and security features are built-in
  • Dependencies and ordering are explicitly managed
  • User timers enable non-root task scheduling

Start migrating critical cron jobs to systemd timers to benefit from modern features while maintaining reliability. Combine timers with other systemd features like path units and socket activation for even more powerful automation patterns.