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

  1. Schedule one-time jobs with at — Use at to schedule a command to run once at a specified future time and manage the at queue
  2. Schedule recurring jobs with cron — Write crontab entries using the five-field time specification and manage crontabs for users and the system
  3. Create and manage systemd timer units — Write a .timer unit file paired with a .service unit to schedule recurring tasks with systemd
  4. 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/
at job output is emailed by default

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 removes all jobs without confirmation

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
Use full paths in crontab commands

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.timer triggers backup.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=true catches up at next boot
Advantages over cron

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
Test by starting the service directly

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 atdsudo dnf install at && sudo systemctl enable --now atd
Schedule a one-time jobecho "COMMAND" | at TIME
List pending at jobsatq
Remove an at jobatrm JOBNUM
Edit current user's crontabcrontab -e
List current user's crontabcrontab -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 timerWrite .service + .timer in /etc/systemd/system/
Activate a timersudo systemctl daemon-reload && systemctl enable --now TIMER.timer
List all timerssystemctl list-timers --all
Test timer's service manuallysudo systemctl start SERVICE.service
Verify calendar expressionsystemd-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.

  1. Write the command to schedule the script /usr/local/bin/report.sh to run once in 2 hours using at.
  2. Write the crontab entry to run /usr/local/bin/cleanup.sh every day at 3:30 AM.
  3. Write the crontab entry to run /usr/local/bin/check.sh every 15 minutes, Monday through Friday.
  4. 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?
  5. 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.
  6. After creating backup.timer and backup.service, write the three commands needed before the timer will fire.

Knowledge Check — Answers

  1. echo "/usr/local/bin/report.sh" | at now + 2 hours
    Also accept the interactive form: at now + 2 hours then typing the command at the at> prompt and pressing Ctrl+D.
  2. 30 3 * * * /usr/local/bin/cleanup.sh
    Fields: minute=30, hour=3, day-of-month=*, month=*, day-of-week=*
  3. */15 * * * 1-5 /usr/local/bin/check.sh
    Fields: minute=*/15 (every 15 min), hour=*, dom=*, month=*, dow=1-5 (Mon-Fri)
  4. Edit /etc/crontab directly with sudo vim /etc/crontab or 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).
  5. [Timer]
    OnCalendar=*-*-* 02:00:00
    Persistent=true
    OnCalendar=*-*-* 02:00:00 fires daily at 2 AM. Persistent=true catches up if the system was off when the timer should have fired.
  6. (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.timer as equivalent to steps 2 and 3.

Key Takeaways

  1. Use at for one-time jobs; cron or systemd timers for recurring. echo "CMD" | at TIME schedules a single future job. atq lists pending jobs; atrm removes them. Ensure atd is running with systemctl enable --now atd.
  2. crontab format: MIN HOUR DOM MON DOW COMMAND. Edit with crontab -e; list with crontab -l. Use full paths in crontab commands — cron's $PATH is minimal. System-wide jobs go in /etc/crontab or /etc/cron.d/ and include a username field.
  3. systemd timers need a paired .service and .timer unit. OnCalendar=*-*-* 02:00:00 for calendar timing. OnBootSec=5min for boot-relative timing. Persistent=true catches missed runs. Always daemon-reload then enable --now after creating units.
  4. Verify and test before trusting the schedule. systemctl list-timers shows next fire time. systemctl start SERVICE.service tests immediately. journalctl -u SERVICE.service shows execution output. systemd-analyze calendar EXPR validates timer syntax.

Graded Lab

  • Ensure atd and crond are running. Use at to schedule date >> /tmp/at-test.txt to run in 2 minutes. Wait and confirm the file was created with cat /tmp/at-test.txt.
  • Use crontab -e to add an entry that runs date >> /tmp/cron-test.txt every minute. Verify with crontab -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/labtest that runs /usr/bin/logger "cron lab test" as root every 5 minutes. Verify by checking /var/log/messages after 5 minutes.
  • Create a systemd timer pair: /etc/systemd/system/labtest.service running /usr/bin/logger "timer lab test" and /etc/systemd/system/labtest.timer with OnBootSec=1min. Run daemon-reload and enable --now labtest.timer.
  • Use systemctl list-timers to verify the timer is scheduled. Test immediately with systemctl start labtest.service. Confirm execution with journalctl -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.
RHCSA Objective

"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.