Set up cron jobs on Fedora

Fedora supports traditional cron via cronie and the modern systemd timer approach, both of which let you schedule recurring tasks reliably on any Fedora system.

You need a task to run automatically, but it keeps failing

You wrote a script to archive old logs or sync a configuration directory. You tested it manually in your terminal and it works perfectly. You set it to run automatically, and then nothing happens. Or worse, it runs but fails silently because the background environment looks completely different from your interactive shell. You need a scheduler that actually works on Fedora, logs its failures, and survives reboots without you babysitting it.

What is actually scheduling your jobs

Fedora ships with two scheduling systems. The first is cronie, the traditional cron daemon that has been the standard since the early days of Unix. It reads a simple text table and executes commands at fixed times. The second is systemd timers, which are part of the modern init system. Timers do not run commands directly. They trigger service units. That separation gives you dependency tracking, structured logging in the journal, and the ability to delay execution until the system is fully ready. Both approaches work. They just solve different problems.

Think of cron like a wall calendar with sticky notes. You write the time and the command, and the daemon checks the list every minute. Think of systemd timers like a project manager. You define a job, declare what it needs to run, set a schedule, and the init system handles execution, logging, and recovery.

Setting up cron jobs with crontab

Start with the user crontab. It is the fastest way to schedule a personal task without touching system directories. Open the editor with the standard command.

crontab -e
# WHY: Opens the current user's crontab in the default terminal editor.
# WHY: Creates the file if it does not exist yet.
# WHY: Automatically validates syntax before saving.

Each line follows a strict five-field format followed by the command. The fields represent minute, hour, day of month, month, and day of week. Use * for any value. Use commas for lists. Use slashes for intervals.

# Run a backup script every day at 2:30 AM
30 2 * * * /home/alice/scripts/backup.sh
# Clear a temp directory every Monday at midnight
0 0 * * 1 /usr/bin/rm -rf /home/alice/tmp/*
# Run every 15 minutes
*/15 * * * * /home/alice/scripts/check-status.sh
# Run once at boot
@reboot /home/alice/scripts/startup.sh

Save and exit. The cron daemon picks up changes immediately. You do not need to restart any service. List your current jobs to verify the syntax parsed correctly.

crontab -l
# WHY: Prints the active crontab to standard output.
# WHY: Fails with an error if the file contains invalid syntax.
# WHY: Useful for quick verification before troubleshooting.

For system-wide cron jobs that must run as root, drop executable scripts into the appropriate directory. Fedora provides /etc/cron.hourly/, /etc/cron.daily/, /etc/cron.weekly/, and /etc/cron.monthly/. The daemon runs everything inside those folders at the specified interval.

sudo cp myjob.sh /etc/cron.daily/
# WHY: Places the script where the daily cron runner expects it.
sudo chmod +x /etc/cron.daily/myjob.sh
# WHY: Grants execute permission so the scheduler can run it.
# WHY: Scripts without execute bits are silently skipped.

Verify cronie is running if you suspect it was disabled.

sudo systemctl status crond
# WHY: Shows whether the daemon is active and running.
# WHY: Displays recent log lines to catch startup failures.
# WHY: Use this before restarting if the service appears dead.

Always use absolute paths in cron. Relative paths break when the working directory is /.

Setting up systemd timers

This is the modern standard for anything that needs reliability, dependency tracking, or structured logging. You need two files: a service unit and a timer unit. They share the same base name. Create them in /etc/systemd/system/. Never edit files in /usr/lib/systemd/system/. Those ship with packages and get overwritten on updates.

Here is how to define the actual work the scheduler will perform.

[Unit]
Description=Daily backup job
# WHY: Human-readable label shown in systemctl status output.

[Service]
Type=oneshot
# WHY: Tells systemd the command runs once and exits.
ExecStart=/home/alice/scripts/backup.sh
# WHY: Absolute path to the script or binary to execute.
User=alice
# WHY: Runs the command as this user instead of root.

Here is how to attach a schedule to that service.

[Unit]
Description=Run backup daily at 2:30 AM
# WHY: Separate description for the timer unit itself.

[Timer]
OnCalendar=*-*-* 02:30:00
# WHY: Calendar event syntax for year-month-day hour:minute:second.
Persistent=true
# WHY: Triggers the service immediately if the machine was off at 2:30.

[Install]
WantedBy=timers.target
# WHY: Ensures the timer starts automatically on boot.

Reload the daemon cache, then enable and start the timer.

sudo systemctl daemon-reload
# WHY: Forces systemd to rescan /etc/systemd/system/ for new units.
# WHY: Required after creating or editing any unit file.
sudo systemctl enable --now my-backup.timer
# WHY: Creates symlinks for boot persistence and starts it immediately.
# WHY: The --now flag combines enable and start in one step.

Check the next scheduled run time and view execution logs.

systemctl list-timers --all | grep my-backup
# WHY: Shows when the timer will fire next and when it last triggered.
journalctl -u my-backup.service
# WHY: Pulls structured logs from the service execution.
# WHY: Use -xeu my-backup.service for better formatting and context.

Reload the daemon before you enable the timer. Systemd will silently ignore new files until you tell it to scan the directory.

Verifying the schedule worked

Scheduling is only half the job. You need to confirm the task actually ran and did what you expected. For cron, check the mail spool or the system journal. Cron sends output to the user's mail by default, but Fedora routes that through systemd-journald. Run journalctl -u crond to see execution records. For systemd timers, the journal is your primary source of truth.

journalctl -xeu my-backup.service
# WHY: The x flag adds explanatory annotations to log lines.
# WHY: The e flag jumps to the end of the journal for recent runs.
# WHY: The u flag filters strictly to the specified service unit.

If you see Finished Daily backup job followed by a clean exit code, the schedule is working. If you see Main process exited, code=exited, status=1/FAILURE, the script itself is failing. Check the script's exit codes and ensure it does not depend on an interactive terminal.

Check the journal before you rewrite the script. The error is usually a missing dependency or a permission denial.

Common pitfalls and what the error looks like

Background schedulers run in a stripped-down environment. Your interactive shell loads ~/.bashrc, sets PATH, and exports variables. Cron and systemd timers do not. This mismatch causes the majority of scheduling failures.

Cron uses a minimal PATH that typically only includes /usr/bin:/bin. If your script calls python3, curl, or ffmpeg without absolute paths, it fails. Fix it by defining the path at the top of the crontab.

PATH=/usr/local/bin:/usr/bin:/bin
30 2 * * * /home/alice/scripts/backup.sh
# WHY: Overrides the default cron PATH before the schedule line.
# WHY: Prevents "command not found" errors for standard binaries.
# WHY: Must appear before any scheduled commands to take effect.

Crontab also requires a trailing newline at the end of the file. If you save without it, the last line is ignored. You will see this exact message when you try to save:

crontab: no changes made to crontab

Add a blank line at the bottom and save again. The parser treats the newline as the terminator for the final entry.

Systemd timers fail when the calendar syntax is invalid. The parser is strict. If you type OnCalendar=daily but the service expects a specific time, or if you miss a hyphen, systemd refuses to load the unit. You will see this in the journal:

Failed to parse calendar specification '*-*-* 2:30:00'

Note the leading zero. Systemd requires 02:30:00, not 2:30:00. Fix the formatting and run systemctl daemon-reload.

SELinux can also block execution if you place scripts in directories without the correct context. If your timer fails with Permission denied despite correct file permissions, check the context with ls -Z. Restore the default context with sudo restorecon -v /path/to/script.sh.

Run the script manually as the target user first. If it fails there, the scheduler will never save it.

When to use cron versus systemd timers

Use crontab when you need a quick personal job that runs a single command or a short script. Use system-wide cron directories when you are distributing simple maintenance scripts across multiple machines and want to avoid unit file management. Use systemd timers when the task depends on network availability, requires structured journal logging, or needs to catch up after a missed run. Use systemd path units when you want to trigger a job based on file changes instead of a clock.

Where to go next