RED HAT ENTERPRISE LINUX
Scheduling Tasks:
at, cron & systemd Timers
Schedule tasks using at, cron and systemd timer units
CIS126RH | RHEL System Administration 1
Mesa Community College
Automated task scheduling is one of the most fundamental administration skills —
nightly backups, weekly log rotation, periodic report generation, and recurring
maintenance all depend on reliable scheduling. RHEL 9 provides three mechanisms:
at for one-time future jobs, cron for recurring jobs
using the traditional UNIX scheduling service, and systemd timer units
for the modern systemd-integrated approach. All three are tested on the RHCSA exam.
Learning Objectives
-
Schedule one-time jobs with at —
Use
atto schedule a command to run once at a specified future time and manage the at queue - Schedule recurring jobs with cron — Write crontab entries using the five-field time specification and manage crontabs for users and the system
-
Create and manage systemd timer units —
Write a
.timerunit file paired with a.serviceunit to schedule recurring tasks with systemd - Verify and troubleshoot scheduled jobs — Confirm jobs are scheduled, check execution logs, and diagnose why a scheduled job did not run
Choosing the Right Scheduling Tool
| Requirement | Best tool | Reason |
|---|---|---|
| Run a command once at a specific future time | at |
Designed for one-time deferred execution |
| Run a script every day at midnight | cron or systemd timer |
Both support recurring schedules; cron syntax is simpler for time-based rules |
| Run a task 5 minutes after each boot | systemd timer | OnBootSec=5min — cron cannot express boot-relative timing |
| Run a job for a specific user on a shared system | cron (user crontab) |
Each user has their own crontab, no root required |
| Periodic maintenance that needs systemd integration | systemd timer | Output captured in journal; dependencies on other units; missed-job recovery |
| Traditional scripts in /etc/cron.daily, weekly, monthly | cron (system crontab) |
Drop files into the directory — no crontab editing required |
at: One-Time Job Scheduling
at schedules a command or script to run once at a specified future time.
The job is placed in a queue and executed by the atd daemon.
# Install at if needed
$ sudo dnf install -y at
$ sudo systemctl enable --now atd
# Schedule a command to run at a specific time
$ at 14:30
at> /usr/local/bin/backup.sh
at> (press Ctrl+D to submit)
job 1 at Mon May 25 14:30:00 2026
# Pass commands via echo or heredoc (non-interactive)
$ echo "/usr/local/bin/backup.sh" | at 14:30
$ at 14:30 <<'EOF'
/usr/local/bin/backup.sh
EOF
# Schedule using relative or natural time specifications
$ at now + 30 minutes
$ at midnight
$ at noon tomorrow
$ at 02:00 2026-06-01
$ at teatime # 4 PM
$ at next monday
Managing at Jobs
# List pending at jobs
$ atq
1 Mon May 25 14:30:00 2026 a student
2 Mon May 25 23:59:00 2026 a root
# Show the commands in a specific job
$ at -c 1
#!/bin/sh
# atrun uid=1000 gid=1000
# mail student 0
...
/usr/local/bin/backup.sh
# Remove (delete) a pending job
$ atrm 1
# Verify the job was removed
$ atq
2 Mon May 25 23:59:00 2026 a root
# at uses /var/spool/at/ to store jobs
$ sudo ls /var/spool/at/
If the scheduled command produces any stdout or stderr output, at
attempts to email the output to the user who created the job. On most RHEL systems
without a local mail server, the output goes to the user's local mailbox in
/var/spool/mail/USERNAME.
cron: Recurring Job Scheduling
crond reads crontab files and executes the specified commands at
the scheduled times. It checks for new jobs every minute.
# The crontab time specification — five fields then the command
# MIN HOUR DOM MON DOW COMMAND
# 0 2 * * * /usr/local/bin/backup.sh
# ^ ^ ^ ^ ^
# | | | | Day of Week (0-7, 0 and 7 = Sunday)
# | | | Month (1-12 or Jan-Dec)
# | | Day of Month (1-31)
# | Hour (0-23)
# Minute (0-59)
# Common examples
0 2 * * * /usr/local/bin/backup.sh # 2:00 AM daily
30 8 * * 1-5 /usr/local/bin/report.sh # 8:30 AM Mon-Fri
*/15 * * * * /usr/local/bin/check.sh # every 15 minutes
0 0 1 * * /usr/local/bin/monthly.sh # midnight, 1st of month
0 */4 * * * /usr/local/bin/sync.sh # every 4 hours
@reboot /usr/local/bin/startup.sh # at every boot
@daily /usr/local/bin/daily.sh # daily at midnight
Managing User Crontabs
Each user has their own crontab file. crontab is the command
for creating, editing, listing, and removing user crontabs.
# Edit the current user's crontab
$ crontab -e # opens in $EDITOR (usually vi)
# List the current user's crontab
$ crontab -l
0 2 * * * /usr/local/bin/backup.sh
*/15 * * * * /usr/local/bin/check.sh
# Remove the current user's crontab (with confirmation)
$ crontab -r
# As root: edit another user's crontab
$ sudo crontab -u alice -e
# As root: list another user's crontab
$ sudo crontab -u alice -l
# User crontab files are stored in /var/spool/cron/USERNAME
$ sudo ls /var/spool/cron/
alice student root
crontab -r deletes the entire crontab file immediately — there is
no undo. Use crontab -l > ~/crontab.bak to back up before removing.
On the exam, always use -l to verify before -r.
System Crontab Files
System-wide cron jobs are configured in /etc/crontab and the
/etc/cron.d/ drop-in directory. The system crontab format includes
a username field.
# /etc/crontab — system crontab (includes USERNAME field)
$ cat /etc/crontab
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
# For details see man 4 crontabs
# Example:
# .------------ minute (0 - 59)
# | .--------- hour (0 - 23)
# | | .------ day of month (1 - 31)
# | | | .--- month (1 - 12)
# | | | | .-- day of week (0 - 7, 0 or 7 = Sunday)
# | | | | |
# * * * * * user-name command to be executed
# Drop-in files in /etc/cron.d/ follow the same format
$ sudo tee /etc/cron.d/backup <<'EOF'
0 2 * * * root /usr/local/bin/backup.sh
EOF
# Drop directories — place scripts here, no crontab editing
/etc/cron.hourly/ # run every hour by run-parts
/etc/cron.daily/ # run once daily
/etc/cron.weekly/ # run once weekly
/etc/cron.monthly/ # run once monthly
cron Environment and Output Handling
cron jobs run in a minimal environment — different from an interactive shell. Scripts that work interactively may fail in cron without explicit configuration.
# cron provides a minimal environment — $PATH is very limited
# Always use full paths in crontab entries
0 2 * * * /usr/bin/find /tmp -mtime +7 -delete # correct
0 2 * * * find /tmp -mtime +7 -delete # may fail
# Set PATH in the crontab file header
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
0 2 * * * backup.sh
# Redirect output to suppress email or capture to a file
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# Discard all output
0 2 * * * /usr/local/bin/backup.sh > /dev/null 2>&1
# Set MAILTO to control where output is sent
MAILTO=admin@example.com # email output here
MAILTO="" # suppress all email
cron's $PATH does not include /usr/local/bin or other
custom directories. Always write the full path to every command and script in a
crontab entry to ensure the job runs correctly.
systemd Timer Units: Overview
systemd timers are the modern alternative to cron. Each timer is paired with a service unit that defines what runs.
- A .timer unit defines when to run (the schedule)
- A .service unit defines what to run (the command)
- Both files must have the same base name:
backup.timertriggersbackup.service - Timers support both calendar (cron-like) and monotonic (elapsed-time) scheduling
- Output is captured in the systemd journal — no email required
- If the system is off when a timer should have fired,
Persistent=truecatches up at next boot
systemd timers can depend on other units, capture output in the journal,
support boot-relative timing (OnBootSec), recover missed jobs
at next boot, and be managed with familiar systemctl commands.
Creating a systemd Timer
A timer requires two unit files with the same base name.
Place them in /etc/systemd/system/.
# Step 1: Create the service unit — defines WHAT to run
$ sudo tee /etc/systemd/system/backup.service <<'EOF'
[Unit]
Description=Nightly backup job
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
EOF
# Step 2: Create the timer unit — defines WHEN to run
$ sudo tee /etc/systemd/system/backup.timer <<'EOF'
[Unit]
Description=Run backup.service daily at 2 AM
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
# Step 3: Reload, enable, and start the timer
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now backup.timer
# Step 4: Verify
$ systemctl status backup.timer
$ systemctl list-timers backup.timer
Timer Time Specifications
systemd timers support two scheduling types: calendar (specific real-world times) and monotonic (elapsed time from a reference point).
Calendar timers (OnCalendar)
OnCalendar=*-*-* 02:00:00 # daily at 2:00 AM
OnCalendar=Mon *-*-* 08:00:00 # every Monday at 8 AM
OnCalendar=weekly # Monday at 00:00 (shortcut)
OnCalendar=daily # daily at midnight
OnCalendar=hourly # every hour on the hour
OnCalendar=*:0/15 # every 15 minutes
OnCalendar=*-*-1 00:00:00 # first of every month
Monotonic timers (elapsed-time based)
OnBootSec=5min # 5 minutes after boot
OnActiveSec=1h # 1 hour after the timer was activated
OnUnitActiveSec=1d # 1 day after the service last ran
# Verify a calendar expression before using it
$ systemd-analyze calendar "*-*-* 02:00:00"
Original form: *-*-* 02:00:00
Normalized form: *-*-* 02:00:00
Next elapse: Mon 2026-05-26 02:00:00 MST
(in UTC): Mon 2026-05-26 09:00:00 UTC
From now: 12h left
Managing and Verifying Timers
# List all active timers with next scheduled run
$ systemctl list-timers
NEXT LEFT LAST PASSED UNIT
Mon 2026-05-26 02:00:00 MST 11h left Sun 2026-05-25 02:00:00 MST 12h ago backup.timer
Mon 2026-05-26 00:00:00 MST 9h left Sun 2026-05-25 00:00:00 MST 14h ago logrotate.timer
# Show all timers including inactive
$ systemctl list-timers --all
# Check timer status
$ systemctl status backup.timer
# Manually trigger the associated service (for testing)
$ sudo systemctl start backup.service
# View journal output from the last timer-triggered run
$ sudo journalctl -u backup.service --since "24 hours ago"
# Disable a timer
$ sudo systemctl disable --now backup.timer
Before waiting for the timer to fire, test by running
sudo systemctl start backup.service directly. This confirms
the service works correctly before trusting the timer to call it.
Troubleshooting Scheduled Jobs
When a scheduled job does not run as expected, a systematic approach identifies the cause quickly.
| Symptom | Tool | Likely cause and fix |
|---|---|---|
| cron job never runs | systemctl status crond |
crond not running — systemctl enable --now crond |
| cron job runs but fails silently | cat /var/spool/mail/USER |
Check cron mail output; usually a PATH or permission problem |
| at job not running | systemctl status atd |
atd not running — systemctl enable --now atd |
| systemd timer not firing | systemctl list-timers --all |
Timer not enabled — systemctl enable --now TIMER.timer |
| systemd service fails when triggered by timer | journalctl -u SERVICE.service |
Check journal for error output; often a script permission or path issue |
| Crontab syntax error | /var/log/cron |
crond logs errors to /var/log/cron — check for "bad minute" or similar |
Scheduling Quick Reference
| Task | Command or configuration |
|---|---|
| Install and enable atd | sudo dnf install at && sudo systemctl enable --now atd |
| Schedule a one-time job | echo "COMMAND" | at TIME |
| List pending at jobs | atq |
| Remove an at job | atrm JOBNUM |
| Edit current user's crontab | crontab -e |
| List current user's crontab | crontab -l |
| Edit another user's crontab (root) | sudo crontab -u USERNAME -e |
| System crontab location | /etc/crontab, /etc/cron.d/ |
| Drop-in script directories | /etc/cron.daily/, cron.weekly/, cron.monthly/ |
| Create a systemd timer | Write .service + .timer in /etc/systemd/system/ |
| Activate a timer | sudo systemctl daemon-reload && systemctl enable --now TIMER.timer |
| List all timers | systemctl list-timers --all |
| Test timer's service manually | sudo systemctl start SERVICE.service |
| Verify calendar expression | systemd-analyze calendar "EXPRESSION" |
Common Mistakes
| Mistake | What goes wrong | Correct approach |
|---|---|---|
| Wrong crontab field order | Job runs at the wrong time or not at all — a very common syntax error | Memorise: MIN HOUR DOM MON DOW — minute is always first |
| Using relative paths in crontab commands | Command not found — cron's PATH is minimal | Always use full absolute paths: /usr/bin/python3, /usr/local/bin/script.sh |
| Script not executable in cron/systemd timer | Permission denied — script exists but cannot be run | chmod +x /path/to/script.sh before scheduling |
| Forgetting daemon-reload after creating a timer unit | systemctl cannot find or enable the new timer | sudo systemctl daemon-reload after any new or modified unit file |
| Creating a timer without enabling it | Timer exists but never fires — not active | sudo systemctl enable --now TIMER.timer to enable and start |
| Forgetting the [Install] section in the timer unit | Timer cannot be enabled with systemctl enable — no WantedBy target | Always include [Install] with WantedBy=timers.target |
Knowledge Check
Answer these before moving to the next slide.
- Write the command to schedule the script
/usr/local/bin/report.shto run once in 2 hours usingat. - Write the crontab entry to run
/usr/local/bin/cleanup.shevery day at 3:30 AM. - Write the crontab entry to run
/usr/local/bin/check.shevery 15 minutes, Monday through Friday. - Write the two commands needed to edit the system crontab file and add a job running as root. What is the file format difference from a user crontab?
- Write the
[Timer]section of a systemd timer unit that runs daily at 2 AM and also catches up if the system was off at that time. - After creating
backup.timerandbackup.service, write the three commands needed before the timer will fire.
Knowledge Check — Answers
echo "/usr/local/bin/report.sh" | at now + 2 hours
Also accept the interactive form:at now + 2 hoursthen typing the command at theat>prompt and pressing Ctrl+D.30 3 * * * /usr/local/bin/cleanup.sh
Fields: minute=30, hour=3, day-of-month=*, month=*, day-of-week=**/15 * * * 1-5 /usr/local/bin/check.sh
Fields: minute=*/15 (every 15 min), hour=*, dom=*, month=*, dow=1-5 (Mon-Fri)- Edit
/etc/crontabdirectly withsudo vim /etc/crontabor add a file in/etc/cron.d/. The system crontab format has six fields — the fifth field is the username to run as:30 3 * * * root /usr/local/bin/cleanup.sh. User crontabs have only five fields (no username field). -
[Timer] OnCalendar=*-*-* 02:00:00 Persistent=true
OnCalendar=*-*-* 02:00:00fires daily at 2 AM.Persistent=truecatches up if the system was off when the timer should have fired. - (1)
sudo systemctl daemon-reload— reload to read the new unit files
(2)sudo systemctl enable backup.timer— configure to start at boot
(3)sudo systemctl start backup.timer— start it now
Accept the combined form:sudo systemctl enable --now backup.timeras equivalent to steps 2 and 3.
Key Takeaways
-
Use
atfor one-time jobs; cron or systemd timers for recurring.echo "CMD" | at TIMEschedules a single future job.atqlists pending jobs;atrmremoves them. Ensureatdis running withsystemctl enable --now atd. -
crontab format:
MIN HOUR DOM MON DOW COMMAND. Edit withcrontab -e; list withcrontab -l. Use full paths in crontab commands — cron's$PATHis minimal. System-wide jobs go in/etc/crontabor/etc/cron.d/and include a username field. -
systemd timers need a paired
.serviceand.timerunit.OnCalendar=*-*-* 02:00:00for calendar timing.OnBootSec=5minfor boot-relative timing.Persistent=truecatches missed runs. Alwaysdaemon-reloadthenenable --nowafter creating units. -
Verify and test before trusting the schedule.
systemctl list-timersshows next fire time.systemctl start SERVICE.servicetests immediately.journalctl -u SERVICE.serviceshows execution output.systemd-analyze calendar EXPRvalidates timer syntax.
Graded Lab
- Ensure
atdandcrondare running. Useatto scheduledate >> /tmp/at-test.txtto run in 2 minutes. Wait and confirm the file was created withcat /tmp/at-test.txt. - Use
crontab -eto add an entry that runsdate >> /tmp/cron-test.txtevery minute. Verify withcrontab -l. Wait 2 minutes and confirm the file exists with multiple timestamps. Remove the entry. - Create a system-level cron job in
/etc/cron.d/labtestthat runs/usr/bin/logger "cron lab test"as root every 5 minutes. Verify by checking/var/log/messagesafter 5 minutes. - Create a systemd timer pair:
/etc/systemd/system/labtest.servicerunning/usr/bin/logger "timer lab test"and/etc/systemd/system/labtest.timerwithOnBootSec=1min. Rundaemon-reloadandenable --now labtest.timer. - Use
systemctl list-timersto verify the timer is scheduled. Test immediately withsystemctl start labtest.service. Confirm execution withjournalctl -u labtest.service --since "5 minutes ago". - Run
systemd-analyze calendar "Mon *-*-* 09:00:00"to verify a calendar expression. Clean up: remove the cron.d file and disable the timer.
"Schedule tasks using at, cron and systemd timer units." Know the at command, the five-field crontab format with full paths, and the two-file systemd timer setup with daemon-reload + enable --now.