How to Schedule Tasks with cron and systemd Timers on Fedora

Schedule tasks on Fedora using systemd timers for system-wide jobs or cron for user-level scripts.

You scheduled a task, and it never ran

You wrote a backup script. You tested it manually, and it works. You added an entry to crontab to run it every night at 2 AM. Three days later, you check the backup directory and the files are missing. You check crontab -l and the schedule looks perfect. You restart the machine, and the script runs once, then stops again.

The problem isn't your script. The problem is the scheduler. Cron runs in a minimal environment that often breaks scripts relying on shell variables. Cron doesn't know if the system was asleep at 2 AM. Cron doesn't integrate with the boot process. Fedora uses systemd as the init system. Systemd timers are the native way to schedule tasks. They handle environment isolation, power management, and missed runs automatically.

Switch to systemd timers for system tasks. Use user timers for personal automation. Cron stays on Fedora for backward compatibility, but it is no longer the recommended tool for new work.

What's actually happening

Cron is a daemon that wakes up every minute, checks a list of jobs, and fires them. It runs jobs in a stripped-down environment. It does not source ~/.bashrc. It has a limited $PATH. If your script depends on environment variables or relative paths, cron fails silently.

Systemd timers are units managed by the init system. A timer unit triggers a service unit. The service unit defines the environment, the user, the working directory, and the command. Systemd tracks the timer state. If the system is off when the timer should fire, the timer can catch up immediately after boot. If the system is suspended, the timer can wake the machine or wait until resume.

Think of cron as a paper schedule taped to a wall. It tells you what should happen, but it doesn't know if the building is open or if the lights are on. Systemd timers are like a digital calendar integrated with the building's management system. It knows the state of the building, it can trigger alarms, and it records exactly when actions happened.

How to set up a systemd timer

Systemd timers require two files. A .timer file defines the schedule. A .service file defines the action. The timer triggers the service. You enable the timer, not the service.

Here's how to create a reliable systemd timer that runs a script daily and catches up if the system was off.

# /etc/systemd/system/backup.timer
[Unit]
Description=Run daily backup script
# WHY: Human-readable description for systemctl status and journalctl output.

[Timer]
OnCalendar=*-*-* 02:00:00
# WHY: Runs every day at 2 AM local time. Use UTC if your system clock is UTC.
Persistent=true
# WHY: If the system is off at 2 AM, the timer fires immediately after boot.
RandomizedDelaySec=300
# WHY: Adds up to 5 minutes of random delay. Prevents thundering herd on clusters.

[Install]
WantedBy=timers.target
# WHY: Tells systemd to start this timer when the timers.target is reached.

Timers trigger services. You need a matching service unit to define what actually runs.

# /etc/systemd/system/backup.service
[Unit]
Description=Daily backup service
# WHY: Describes the service action for logs and status checks.

[Service]
Type=oneshot
# WHY: The service runs a command and exits. Systemd considers it done when the process ends.
ExecStart=/usr/local/bin/backup.sh
# WHY: Absolute path required. Relative paths fail because systemd runs in an empty environment.
User=backup-user
# WHY: Run as a dedicated user instead of root. Limits damage if the script fails.
WorkingDirectory=/var/backups
# WHY: Sets the current directory. Scripts that use relative paths need this.

[Install]
WantedBy=multi-user.target
# WHY: Optional. Only needed if you want to start the service manually via systemctl start.

Enable the timer and verify the schedule.

sudo systemctl daemon-reload
# WHY: Reloads unit files. Required after creating or editing files in /etc/systemd/system/.
sudo systemctl enable --now backup.timer
# WHY: Enables the timer to start at boot and starts it immediately.
systemctl list-timers --all
# WHY: Shows all active and inactive timers. Check the "NEXT" column to confirm the schedule.

Edit files in /etc/systemd/system/. Never edit files in /usr/lib/systemd/system/. Package updates overwrite /usr/lib/. Your changes vanish.

Enable the timer, not the service. The timer owns the schedule.

Verify it worked

Run systemctl status backup.timer. Look for Active: active (waiting). If the status shows inactive (dead), the timer isn't running. Check the journal for errors.

journalctl -u backup.timer
# WHY: Shows logs for the timer unit. Look for trigger events and errors.
journalctl -u backup.service
# WHY: Shows logs for the service unit. Check the output of the script here.

If you want to force a run for testing, start the service manually.

sudo systemctl start backup.service
# WHY: Runs the service immediately. Does not update the timer state.

Starting the service manually does not reset the timer. The timer tracks its own schedule. If you need to reset the timer, use systemctl reset-failed backup.timer.

Run systemctl list-timers. If it's not in the list, it's not running.

User timers vs system timers

Cron supports user-level jobs via crontab -e. Systemd supports user-level timers via user instances. User timers run as your user without sudo. They live in ~/.config/systemd/user/.

Create a user timer in ~/.config/systemd/user/mytask.timer. Create a matching service in ~/.config/systemd/user/mytask.service. Enable it with systemctl --user enable mytask.timer.

User timers start when you log in. They stop when you log out. If you need a user timer to run while you are logged out, you must enable lingering.

loginctl enable-linger $USER
# WHY: Keeps the user systemd instance running after logout. Required for background user timers.

User timers replace crontab -e for most needs. They integrate with your user environment and provide better logging.

Common pitfalls and errors

Scripts fail in systemd for different reasons than in cron. The environment is stricter.

Relative paths in ExecStart. Systemd does not inherit your shell's $PATH. ExecStart=script.sh fails with Unit not found or Permission denied. Always use absolute paths. Run which script.sh to find the path.

Missing Type=oneshot. If you omit the type, systemd defaults to simple. It expects the process to stay running. A script that exits immediately will be marked as failed. Use Type=oneshot for scripts that run and exit.

SELinux denials. If the script accesses files, SELinux might block it. The error appears in the journal.

# Example SELinux denial in journalctl
type=AVC msg=audit(1678886400.000:123): avc:  denied  { execute } for  pid=456 comm="backup.sh" name="backup.sh" dev="sda1" ino=789 scontext=system_u:system_r:systemd_service_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0

Read the denial message. It tells you which file was blocked and what access was denied. Fix the context with restorecon -Rv /path/to/script. Do not disable SELinux. SELinux protects you. Read the denial message. Fix the policy, don't disable the shield.

Complex calendar syntax. OnCalendar supports complex schedules. Mon..Fri runs Monday through Friday. *:0/15 runs every 15 minutes. If the syntax is wrong, the timer might never fire.

systemd-analyze calendar 'Mon..Fri 02:00:00'
# WHY: Validates the calendar expression and prints the next trigger time.

Use systemd-analyze calendar to verify schedules. If the output is wrong, your syntax is wrong.

When to use this vs alternatives

Use systemd timers when you need system-wide tasks that integrate with boot, shutdown, and power management. Use systemd timers when you want automatic catch-up for missed runs via Persistent=true. Use systemd timers when you need precise logging and dependency tracking through journalctl and unit dependencies. Use user timers when you need personal automation that runs as your user without root privileges. Use cron when you are migrating from another Linux distribution and need to preserve existing crontab files. Use at for one-off jobs that run once at a specific future time. Avoid cron for system services. Cron lacks dependency management and runs in a limited environment that often breaks scripts relying on environment variables.

Pick the tool that matches the scope. System-wide tasks belong to systemd. User scripts can stay in cron.

Where to go next