Using Systemd Timers to Schedule Recurring System Jobs

Most Linux administrators are familiar with cron for scheduling recurring jobs. Cron works great for simple user-level tasks. But when you're scheduling operating system-level services or long-running processes that need careful management, systemd timers are a better choice.

Systemd timers provide several advantages over cron that make them worth the extra setup:

Job overlap prevention: Systemd timers guarantee that your scheduled job won't start if the previous instance is still running. Cron has no built-in protection against overlapping executions.

Better logging: All timer executions are logged to the systemd journal with timestamps for when the job ran, how long it took, and what the exit status was. You get visibility that cron simply doesn't provide.

Manual invocation: You can trigger the job on-demand without waiting for the schedule. For example, if you need an immediate backup before maintenance, you can run it instantly.

Integration with systemd dependencies: You can configure timers to run after other systemd units, ensuring prerequisites are met. This isn't possible with cron.

This guide shows you how to create a systemd timer for a Python backup script that runs every night. The same approach works for any recurring system-level job.

Prerequisites

  • Root or sudo access to your Linux server
  • A script or command you want to run on a schedule (executable)
  • Basic understanding of systemd units

Step 1: Create Your Executable Script

First, create the Python script that will be run by the timer. This example is a simple database backup script.

Create the script file:

sudo vim /usr/local/bin/backup-database.py

Add the following content:

#!/usr/bin/env python3

import subprocess
import sys
from datetime import datetime

def backup_database():
    """Backup the PostgreSQL database to a timestamped file."""
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_file = f"/var/backups/database_{timestamp}.sql.gz"

    try:
        # Run pg_dump and compress the output
        result = subprocess.run(
            f"pg_dump -U postgres myapp_db | gzip > {backup_file}",
            shell=True,
            check=True,
            capture_output=True,
            text=True
        )
        print(f"Backup completed successfully: {backup_file}")
        return 0
    except subprocess.CalledProcessError as e:
        print(f"Backup failed: {e.stderr}", file=sys.stderr)
        return 1

if __name__ == "__main__":
    sys.exit(backup_database())

Make the script executable:

sudo chmod +x /usr/local/bin/backup-database.py

Test it manually to ensure it works:

sudo /usr/local/bin/backup-database.py

Step 2: Create the Systemd Service File

The service file defines what command to execute when the timer triggers it.

Create the service file:

sudo vim /etc/systemd/system/backup-database.service

Add the following content:

[Unit]
Description=Backup the application database
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-database.py
User=postgres
StandardOutput=journal
StandardError=journal

Let's break down what each line does:

Description: A human-readable description of what this service does.

After=network.target: Ensures the network is available before running (useful if you're backing up to a remote location).

Type=oneshot: Tells systemd this service runs once and exits, rather than continuously. This is appropriate for scripts that complete their work and stop.

ExecStart: The full path to the script or command to execute.

User=postgres: Run the script as the postgres user (adjust this to the appropriate user for your script).

StandardOutput=journal and StandardError=journal: Send all script output to the systemd journal for logging.

Step 3: Create the Systemd Timer File

The timer file defines the schedule for when the service should run.

Create the timer file:

sudo vim /etc/systemd/system/backup-database.timer

Add the following content:

[Unit]
Description=Run the database backup daily at 2 AM
Requires=backup-database.service

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

[Install]
WantedBy=timers.target

Here's what each setting does:

Description: Describes what this timer does.

Requires=backup-database.service: Requires the backup-database.service to exist.

OnCalendar=daily: Run daily. The second OnCalendar line specifies the exact time (2:00 AM).

OnCalendar=--* 02:00:00: Calendar format specifying year-month-day hour:minute:second. The * means any value for that field. This runs every day at 2 AM.

Persistent=true: If the system was shut down when the timer should have run, execute the job when the system boots back up.

WantedBy=timers.target: Makes this timer start on boot.

Common OnCalendar Formats:

  • daily - Every day at midnight
  • *-*-* 02:00:00 - Every day at 2 AM
  • Mon *-*-* 00:00:00 - Every Monday at midnight
  • *-*-1 03:00:00 - First day of every month at 3 AM
  • *-01-01 00:00:00 - New Year's Day at midnight
  • hourly - Every hour on the hour
  • *-*-* 09,17:00:00 - 9 AM and 5 PM every day

Step 4: Start and Enable the Timer

Reload systemd to recognize the new service and timer files:

sudo systemctl daemon-reload

Start the timer:

sudo systemctl start backup-database.timer

Enable the timer to persist after reboot:

sudo systemctl enable backup-database.timer

Verify the timer is running:

sudo systemctl status backup-database.timer

You should see output similar to this:

 backup-database.timer - Run the database backup daily at 2 AM
     Loaded: loaded (/etc/systemd/system/backup-database.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Mon 2026-03-09 12:30:00 UTC; 2s ago
    Trigger: Tue 2026-03-10 02:00:00 UTC; 13h left
  Triggers:  backup-database.service

Step 5: Verify and Monitor

List all active timers on your system:

sudo systemctl list-timers

This shows all running timers, when they last ran, and when they'll run next.

To see the journal logs from your timer (useful for debugging):

sudo journalctl -u backup-database.service -n 20

This shows the last 20 entries from the backup service. Follow logs in real-time:

sudo journalctl -u backup-database.service -f

Step 6: Manually Trigger the Job

You can run the backup job immediately without waiting for the scheduled time:

sudo systemctl start backup-database.service

This is useful for testing, or if you need an immediate backup before maintenance.

Troubleshooting

Timer isn't running: Check that you've enabled it with systemctl enable backup-database.timer and that the service file exists.

Service keeps failing: Check the journal logs with journalctl -u backup-database.service to see the error messages.

Calendar format errors: Test your OnCalendar format with systemd-analyze calendar 'your-format':

systemd-analyze calendar '*-*-* 02:00:00'

Overlapping executions: If you're concerned about a long-running job, add a check in your script to see if another instance is already running, or use Type=oneshot to prevent concurrent runs.

When to Use Systemd Timers vs. Cron

Use systemd timers when:

  • Running operating system-level services or daemons
  • You need better visibility into job execution history
  • You want to prevent job overlap
  • Immediate manual triggering is important
  • The job is critical to your infrastructure

Use cron when:

  • Running user-level jobs or scripts
  • You only need simple scheduling
  • The job runs intermittently and doesn't need close monitoring

Systemd timers are a powerful evolution of cron that integrate seamlessly with the rest of your Linux system. With the journal providing detailed logging and systemd's built-in safety features, you get a more robust and maintainable approach to scheduling critical infrastructure jobs.