15.1 Schedule Tasks with at, cron, and systemd Timers
Key Takeaways
- Use at for one-shot future jobs, cron (crontab -e or /etc/crontab and /etc/cron.*) for recurring schedules, and systemd .timer/.service unit pairs for modern persistent timers on RHEL 10.
- User crontabs are edited with crontab -e; system-wide schedules live in /etc/crontab and drop-in directories—field order and user column differ between user crontab and /etc/crontab.
- systemd timers require a matching service unit (or existing unit), a .timer with OnCalendar/OnBootSec-style triggers, then systemctl enable --now name.timer and list-timers verification.
- Always verify jobs with atq/atrm, crontab -l, systemctl list-timers, and logs (journalctl -u, /var/log/cron) so EX200 end state is demonstrable.
- Schedules must persist across reboot when the task requires it—prefer installed crontab entries, /etc files, or enabled timers over ad-hoc shells that die with the session.
15.1 Schedule Tasks with at, cron, and systemd Timers
Quick Answer: Run a command once in the future with
at. Run recurring jobs withcrontab -e(per user) or/etc/crontab//etc/cron.d(system). Prefer systemd.timer+.serviceunit pairs for modern persistent schedules: write units under/etc/systemd/system/, thensystemctl enable --now name.timer. Verify withatq,crontab -l, andsystemctl list-timers.
Why scheduling is a deploy objective
Under Deploy, configure, and maintain systems, Red Hat expects you to schedule tasks so work runs later or repeatedly without an interactive shell. EX200 tasks look like:
- “At 23:00 tonight, run this backup script once.”
- “Every weekday at 02:15, as root, run
/usr/local/bin/cleanup.sh.” - “Create a systemd timer that runs
myscript.serviceevery hour and enable it.”
You are graded on the configured schedule and ownership, not on watching the clock until the job fires—though you should know how to test (near-term at, temporary cron minute, systemctl start name.service for the timer’s unit).
Three tool families coexist on RHEL 10:
| Tool | Best for | Persistence |
|---|---|---|
| at / batch | One-time future execution | Job queue until run (or removed) |
| cron | Classic recurring calendar schedules | User crontab or /etc files |
| systemd timers | Native systemd schedules, boot-relative and calendar | Enabled .timer units |
Know all three; the exam may specify which to use or leave the choice open when any correct persistent method works.
One-shot jobs with at
Package and service
rpm -q at
sudo dnf install -y at # if missing on a minimal image
sudo systemctl enable --now atd
systemctl is-active atd
Without atd running, at jobs do not execute.
Submit a job
at 23:00
# at> /usr/local/bin/backup.sh
# at> <EOT> # Ctrl-D
Or pipe:
echo "/usr/local/bin/backup.sh" | at 23:00
echo "/usr/local/bin/backup.sh" | at now + 5 minutes
echo "/usr/local/bin/backup.sh" | at 2:30am tomorrow
echo "/usr/local/bin/backup.sh" | at teatime # 16:00 on many systems
Time formats at accepts are flexible (HH:MM, now + N minutes/hours/days, weekday names). Prefer explicit times under exam pressure.
Inspect and remove
atq # list pending jobs (job number, time, user)
at -c JOBNUMBER # show script content of a job
atrm JOBNUMBER # remove
# or: at -d JOBNUMBER
Exam tip: After submitting, immediately atq and at -c to prove the command body is correct. Wrong path or missing execute bit fails silently later.
batch and permissions (awareness)
batch runs when load is low. Access control historically uses /etc/at.allow and /etc/at.deny. On the exam you typically work as root or a permitted user—if at is denied, check allow/deny files and that atd is active.
ls /etc/at.allow /etc/at.deny 2>/dev/null
Environment note
at jobs run with a limited environment; use absolute paths for binaries and scripts. Redirect output if you need proof files:
echo "/usr/local/bin/backup.sh > /var/tmp/backup.out 2>&1" | at now + 2 minutes
Recurring jobs with cron
User crontab: crontab -e
Each user (including root) has a personal crontab managed only via the crontab command—not by hand-editing spool files.
crontab -e # edit current user's crontab
sudo crontab -e -u bob # edit bob's crontab (as root)
crontab -l # list
crontab -r # remove entire crontab (dangerous—confirm first)
crontab -l -u bob
User crontab fields (no user column):
# min hour dom month dow command
# 0-59 0-23 1-31 1-12 0-7 (0 and 7 = Sunday)
15 2 * * 1-5 /usr/local/bin/cleanup.sh
| Field | Meaning | Examples |
|---|---|---|
| minute | 0–59 | 0, */15, 0,30 |
| hour | 0–23 | 2, 9-17 |
| day of month | 1–31 | 1, */2 |
| month | 1–12 or names | 1, jan |
| day of week | 0–7 (Sun) or names | 1-5, mon |
# Examples in a user crontab
0 3 * * * /usr/local/bin/nightly.sh
*/10 * * * * /usr/local/bin/poll.sh
0 9 1 * * /usr/local/bin/monthly.sh
30 4 * * 0 /usr/local/bin/weekly-sun.sh
Comments start with #. Leave a trailing newline. Syntax errors can cause crontab -e to reject the file when you save—read the error and fix before exiting.
System crontab: /etc/crontab
sudo less /etc/crontab
Typical layout:
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue ...
# | | | | |
# * * * * * user-name command to be executed
Critical difference: /etc/crontab and files under /etc/cron.d/ include a user field before the command. User crontabs from crontab -e do not.
# /etc/crontab or /etc/cron.d/myclean
15 2 * * 1-5 root /usr/local/bin/cleanup.sh
If you put a user field in a personal crontab, cron mis-parses the line. If you omit the user in /etc/crontab, the job is wrong.
Drop-in and periodic directories
| Path | Role |
|---|---|
/etc/crontab | Main system crontab |
/etc/cron.d/* | Package/admin drop-in crontabs (same 7-field style with user) |
/etc/cron.hourly/ | Scripts run hourly (run-parts style) |
/etc/cron.daily/ | Daily |
/etc/cron.weekly/ | Weekly |
/etc/cron.monthly/ | Monthly |
/var/spool/cron/<user> | Spool for user crontabs—do not edit by hand |
sudo ls /etc/cron.d /etc/cron.daily
# Install a system job:
sudo tee /etc/cron.d/rhcsa-clean <<'EOF'
# Run cleanup weekdays at 02:15 as root
15 2 * * 1-5 root /usr/local/bin/cleanup.sh
EOF
sudo chmod 644 /etc/cron.d/rhcsa-clean
Scripts in cron.daily etc. must be executable and typically not contain dots in ways that confuse run-parts (distribution-specific; prefer clear names without extension issues). For exam clarity, an explicit line in /etc/cron.d or a user crontab is often safer than fighting run-parts rules.
crond service
systemctl status crond
sudo systemctl enable --now crond
On RHEL the daemon is crond. If it is stopped, no cron jobs run.
Logging and mail
sudo grep CRON /var/log/cron 2>/dev/null | tail
sudo journalctl -u crond -b --no-pager | tail
Cron may mail output to the user (MAILTO). Redirect to files when the task wants an artifact:
0 3 * * * /usr/local/bin/nightly.sh >> /var/log/nightly.log 2>&1
systemd timers (timer + service pairs)
Timers are first-class systemd units. A timer unit activates another unit (almost always a service) on a schedule.
Minimal pair under /etc/systemd/system
Service (/etc/systemd/system/cleanup.service) — what to run:
[Unit]
Description=RHCSA cleanup oneshot service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/cleanup.sh
Timer (/etc/systemd/system/cleanup.timer) — when to run:
[Unit]
Description=Run cleanup.service on a schedule
[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now cleanup.timer
systemctl list-timers --all
systemctl status cleanup.timer
systemctl status cleanup.service
OnCalendar vs monotonic triggers
| Directive | Meaning |
|---|---|
OnCalendar= | Wall-clock schedule (like cron, richer syntax) |
OnBootSec= | Time after boot |
OnUnitActiveSec= | Time after the unit last activated |
OnStartupSec= | After systemd startup |
Persistent=true | Catch up missed runs while powered off (when applicable) |
Unit= | Explicit unit to activate if name differs from timer prefix |
[Timer]
OnBootSec=10min
OnUnitActiveSec=1h
# Calendar examples (systemd.time)
OnCalendar=hourly
OnCalendar=daily
OnCalendar=Mon..Fri 02:15
OnCalendar=*-*-* 00/2:00:00
Validate calendar expressions:
systemd-analyze calendar 'Mon..Fri 02:15'
systemd-analyze calendar '*-*-* 02:15:00'
Enable, start, and test
sudo systemctl enable cleanup.timer # start timer at boot
sudo systemctl start cleanup.timer # arm timer now
# equivalent:
sudo systemctl enable --now cleanup.timer
# Manually run the payload once (does not wait for calendar):
sudo systemctl start cleanup.service
sudo journalctl -u cleanup.service -n 20 --no-pager
systemctl list-timers
systemctl list-timers --all
systemctl cat cleanup.timer
systemctl cat cleanup.service
list-timers shows NEXT and LEFT—proof the timer is armed. After reboot, re-check that the timer is active and enabled.
Where unit files live
| Location | Use |
|---|---|
/etc/systemd/system/ | Admin-created units (exam default) |
/usr/lib/systemd/system/ | Vendor units—prefer drop-ins over editing |
/etc/systemd/system/name.timer.d/ | Drop-in overrides |
Do not put custom timers only in /tmp. They must be on disk under systemd paths to persist.
Choosing at vs cron vs timer
| Requirement | Prefer |
|---|---|
| Once at a specific future time | at |
| Simple recurring line, classic style | crontab -e or /etc/cron.d |
| Boot-relative (“10 minutes after every boot”) | systemd timer OnBootSec= |
| Tight integration with other units / logging | systemd timer |
| Task explicitly says “cron” or “timer” | Follow the wording |
If the task says systemd timer, deliver a .timer + .service pair, not only a crontab.
Exam workflows
Workflow A — One-time job in five minutes
sudo systemctl enable --now atd
echo "/usr/local/bin/job.sh >> /var/tmp/job.out 2>&1" | at now + 5 minutes
atq
Workflow B — User recurring job
crontab -e
# add: 0 * * * * /usr/local/bin/hourly-user.sh
crontab -l
Workflow C — System job as root via /etc/cron.d
sudo tee /etc/cron.d/hourly-root <<'EOF'
0 * * * * root /usr/local/bin/hourly-root.sh
EOF
sudo systemctl is-active crond || sudo systemctl enable --now crond
Workflow D — systemd hourly timer
sudo tee /etc/systemd/system/hourly-job.service <<'EOF'
[Unit]
Description=Hourly job service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/hourly-root.sh
EOF
sudo tee /etc/systemd/system/hourly-job.timer <<'EOF'
[Unit]
Description=Hourly job timer
[Timer]
OnCalendar=hourly
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now hourly-job.timer
systemctl list-timers | grep hourly-job
Common traps
- Forgetting the user field in
/etc/crontab/cron.d, or adding a user field incrontab -e. - Editing
/var/spool/cron/*by hand instead ofcrontab -e. - Relative paths in jobs—use absolute paths to scripts and interpreters.
- Script not executable or wrong SELinux context—test with manual run first.
atdorcronddisabled—schedule exists but never fires.- Only writing a
.timerwithout a service (or wrongUnit=name). - Creating units but skipping
daemon-reloadandenable --now. - Assuming
systemctl start foo.timerenables at boot—useenablefor persistence. - Day-of-week and day-of-month both restricted in cron—behavior is OR in classic cron; keep schedules simple on the exam.
- Not verifying with
crontab -l,atq, orlist-timersbefore moving on.
Relationship to other chapters
- Logs (Ch10):
journalctl -u name.serviceand/var/log/cronprove execution attempts. - Services (15.2): timers are units—same
enable/start/statusdiscipline. - Shell scripting (Ch07): scheduled payloads are often your scripts—test them interactively first.
- Permissions/SELinux: scheduled root jobs still hit DAC and SELinux on target files.
Section checkpoint
You should submit and manage at jobs with atq/atrm, edit user crontabs with crontab -e, write correct system lines in /etc/crontab or /etc/cron.d (including the user column), ensure atd/crond are running, build systemd timer/service pairs, daemon-reload, enable --now *.timer, and verify with list-timers and logs. That is the EX200 bar for scheduling tasks on RHEL 10.
Which command opens the current user's crontab for editing the correct way on RHEL?
How does a job line in /etc/crontab differ from a line in a personal crontab created with crontab -e?
You created /etc/systemd/system/backup.service and backup.timer. Which sequence best arms the schedule now and at boot?
Which tool is most appropriate for a single command that must run once tomorrow at 03:00 and never again?