Scheduling jobs: cron, at and systemd timers
-
Phase 0: Before you start
-
Phase 1: The origins and OS architecture
-
Phase 2: Day-to-day administration and the command builder
-
Phase 3: Enterprise infrastructure and storage
-
Phase 4: Troubleshooting step by step
-
Phase 5: Final project and career
Linux cron jobs are the oldest and most common way to make a server do something on a schedule: rotate a file at midnight, send a report every weekday at 9, clean a folder every Sunday. Nearly every server you inherit at work will have a few crontab lines, and many of them will have small mistakes that nobody noticed because cron fails quietly.
In Lesson 27 you scheduled disk-report with a systemd timer. Here you learn the other tools: crontab lines for users, /etc/cron.d files for the system, anacron for machines that aren’t always on, and at for one-off jobs. You’ll also break cron the two ways it breaks most often, PATH and the % sign, and decide when a timer is the better choice.
Where this lesson fits
Lesson 28 of 82 in Linux Administration · Phase 2: Day-to-day administration and the command builder · Before this: Lesson 27, write your own service and timer · Next: Lesson 29, logging with journald, rsyslog and logrotate
| Time | You need | By the end you can |
|---|---|---|
| About 60 minutes | rocky01 with Lesson 27 done (disk-report.sh and its timer exist); ubu01 for the Ubuntu notes | Write and read crontab lines, add a system job in /etc/cron.d, explain how cron.daily runs on Rocky, queue jobs with at, and fix the PATH and % problems from the logs |
What it is, in one line
cron runs commands at fixed times from crontab lines, anacron makes sure daily and weekly jobs still run on machines that were switched off, and at runs a command once at a time you choose.
In Simple Words
The school bell, the sticky note and the late register
A school runs on a bell timetable: first bell at 8:00, lunch at 12:30, last bell at 3:15, every working day without anyone pressing a button. If the timetable is written wrongly, the bell rings at the wrong time and nobody knows why until the children are standing in the corridor.
The head teacher also leaves sticky notes: “call the electrician at 4 today”. That happens once and the note goes in the bin. And when a teacher is on leave on the day the register must be filled, she fills it first thing the morning she returns.
The bell timetable is cron, the sticky note is at, and the late register is anacron.
Engineer's View
The crontab line and where cron jobs live
A crontab line has five time fields and then the command. cron checks every line once a minute and runs the ones whose fields all match the current time.
| Field | Allowed values | Examples |
|---|---|---|
| minute | 0-59 | 0, */15 (every 15 minutes), 5,35 |
| hour | 0-23 | 2, 9-18 (every hour from 9 to 18) |
| day of month | 1-31 | 1 (the 1st), * |
| month | 1-12 or jan-dec | *, 1,4,7,10 |
| day of week | 0-7 (0 and 7 are Sunday) or sun-sat | 1-5 (Monday to Friday) |
So 30 2 * * * is every day at 02:30 and 0 9 * * 1-5 is 9:00 on weekdays. Shortcuts such as @daily, @weekly and @reboot exist too. If you set both day of month and day of week, cron runs when either matches, which surprises almost everyone.
| Where | Who edits it | User field? | Notes |
|---|---|---|---|
| Your crontab | crontab -e, crontab -l, crontab -r | No, runs as you | Stored in /var/spool/cron/USER on Rocky, /var/spool/cron/crontabs/USER on Ubuntu. Never edit those files directly |
/etc/crontab | root, with an editor | Yes | The system crontab. Packages may replace it, so keep your jobs elsewhere |
/etc/cron.d/NAME | root, one file per job or app | Yes | Picked up automatically. Use only letters, digits, – and _ in the name: Ubuntu ignores names with dots |
/etc/cron.daily/ and friends | root, executable scripts | No | Run by run-parts. On Rocky anacron runs daily, weekly and monthly; on Ubuntu /etc/crontab runs them at 06:25 unless anacron is installed |
Rocky Linux 10 uses cronie (service crond, with cronie-anacron), and Ubuntu 26.04 uses Debian’s cron (service cron). A job gets a small environment, not your login shell: SHELL is /bin/sh, HOME is your home folder, and on Rocky PATH is a short default with no /usr/local/bin. Ubuntu changed this in 24.04: its cron.service starts cron with -P, so jobs inherit systemd’s PATH, which does include /usr/local/bin.
Output that a job prints is mailed to the owner, and on a server with no mail program installed it’s thrown away.
How it works, step by step
Here is the path from a line you save in your crontab to a command running at the right minute.
1crontab saves your lines where cron can read them
crontab -e opens a temporary copy in your editor, checks the syntax when you save and installs it in the spool folder. A bad line is rejected with “errors in crontab file, can’t install” and nothing changes, which is a good reason to use crontab -e instead of writing into the spool folder yourself. Files in /etc/cron.d and /etc/crontab are read directly; cronie notices changes straight away and Debian’s cron checks once a minute.
2Every minute, cron forks a shell for each matching line
At the start of each minute the daemon compares the time with every line. For each match it forks, switches to the job’s user, builds the small environment and runs the command with /bin/sh -c. The diagram below follows one line through those steps.
3Output goes to mail unless you send it somewhere
Anything the command prints is collected and mailed to the crontab owner, or to the address in a MAILTO= line. Minimal servers have no mail program, so the output, including every error message, is lost. Always end a job with >> /path/to/file.log 2>&1, or pipe it to logger -t jobname so it lands in the journal.
4The % sign means something else in a crontab
cron cuts the command at the first unescaped %, runs the part before it, and feeds the rest to the command as input with every further % turned into a new line. So date +%F in a crontab line breaks the command in half, along with any redirect that came after it. Write \% inside crontab lines, or move the command into a script where % is ordinary.
5anacron catches up on machines that were off
cron only runs a job if the machine is on at that minute. anacron works in days instead: it keeps a timestamp per job and runs any daily, weekly or monthly job that is overdue. On Rocky, /etc/cron.d/0hourly runs /etc/cron.hourly every hour at minute 01, the 0anacron script there starts anacron, and anacron runs /etc/cron.daily after a random delay, only between 03:00 and 22:00. The picture below compares the four schedulers side by side.
6at runs something once
The at daemon (atd) keeps a queue of one-off jobs. at 23:30 or at now + 2 hours reads commands from your terminal or a pipe, copies your current environment and folder into the job, and runs it once. atq lists the queue and atrm deletes a job. It’s the right tool for “restart this after the backup finishes tonight” without leaving a crontab line behind.
7Pick the right scheduler
| You need | Use |
|---|---|
| A simple repeat on a server that’s always on, or a job other teams expect to find in crontab | cron |
| A daily or weekly job on a laptop or VM that is often off | anacron (cron.daily on Rocky) or a timer with Persistent=true |
| One run at a set time, then forget it | at |
| Logs in the journal, dependencies, resource limits, alerts with OnFailure=, randomised start across many servers | A systemd timer (Lesson 27) |
Hands-on lab: schedule, trace and fix cron jobs on rocky01
Before you run any command, double-check it
Read every command twice before you press Enter. Check the target: the server name, the user, the disk or device name, the path and the sizes. These commands are for learning and were tested in our lab, not on your system. At work, run the read-only commands first, follow your change process, make sure a backup exists, and ask a second person to check anything that changes or deletes data. You run them at your own risk.
You’ll work on rocky01, mostly as student in a folder called ~/lab28, with one system job in /etc/cron.d that you remove at the end. cron jobs need real minutes to pass, so start the lab when you have time to wait a few minutes between steps. Create the folder first with mkdir -p ~/lab28 && cd ~/lab28.
1Check the cron daemon and your crontab
Run this:
systemctl is-active crond
crontab -l
cat /etc/crontabYou should see something like this:
active
no crontab for student
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
# For details see man 4 crontabs
# 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,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executedcrond is running, student has no crontab yet, and /etc/crontab on Rocky holds only variables and a reminder of the field order (keep that comment block in mind, it’s the quickest reference on any server). On ubu01 the service is cron and /etc/crontab also contains the lines that run cron.hourly, daily, weekly and monthly.
✅ Checkpoint: crond is active and crontab -l says there is no crontab for student.
2Write a small job and add it to your crontab
Paste this whole block:
cat > ~/lab28/stamp.sh <<'EOF'
#!/bin/bash
# stamp.sh: write one line with the time, the user and the PATH this job got
echo "$(date '+%F %T') stamp from $(whoami), PATH=$PATH"
EOF
chmod 755 ~/lab28/stamp.sh
~/lab28/stamp.sh
(crontab -l 2>/dev/null; echo '*/2 * * * * /home/student/lab28/stamp.sh >> /home/student/lab28/stamp.log 2>&1') | crontab -
crontab -lYou should see something like this:
2026-10-27 12:10:42 stamp from student, PATH=/home/student/.local/bin:/home/student/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
*/2 * * * * /home/student/lab28/stamp.sh >> /home/student/lab28/stamp.log 2>&1Run by hand, the script sees your login PATH, with /usr/local/bin in it. The crontab line runs it every two minutes and appends both output and errors to stamp.log. The (crontab -l; echo ...) | crontab - form adds a line without opening an editor, which is handy in scripts.
For everyday edits use crontab -e, which opens vi on Rocky (or whatever $EDITOR says) and checks the line when you save.
Try it yourself: build a crontab line, with the right fields and a log redirect, in the Command Builder.
✅ Checkpoint: crontab -l shows your one line.

3Check that the job ran
Wait about four minutes, then run this:
cat ~/lab28/stamp.log
sudo journalctl -u crond --since '-6min' --no-pager | grep stampYou should see something like this:
2026-10-27 12:12:01 stamp from student, PATH=/usr/bin:/bin:/usr/sbin:/sbin
2026-10-27 12:14:01 stamp from student, PATH=/usr/bin:/bin:/usr/sbin:/sbin
Oct 27 12:12:01 rocky01 CROND[2681]: (student) CMD (/home/student/lab28/stamp.sh >> /home/student/lab28/stamp.log 2>&1)
Oct 27 12:12:01 rocky01 CROND[2679]: (student) CMDEND (/home/student/lab28/stamp.sh >> /home/student/lab28/stamp.log 2>&1)
Oct 27 12:14:01 rocky01 CROND[2712]: (student) CMD (/home/student/lab28/stamp.sh >> /home/student/lab28/stamp.log 2>&1)
Oct 27 12:14:01 rocky01 CROND[2710]: (student) CMDEND (/home/student/lab28/stamp.sh >> /home/student/lab28/stamp.log 2>&1)Look at the PATH in the log: under cron it’s a short default, and /usr/local/bin is missing. Remember that for the Break-It section. Each run leaves a CMD line when it starts and a CMDEND line when it ends, also in /var/log/cron on Rocky.
On ubu01 use journalctl -u cron; the lines are tagged CRON there, and the PATH shows /usr/local/bin because Ubuntu’s cron inherits systemd’s PATH.
✅ Checkpoint: stamp.log has a new line every two minutes and the journal shows matching CMD lines.
4Add a system job in /etc/cron.d
Run this:
echo '*/5 * * * * root /usr/local/bin/disk-report.sh >> /var/log/disk-report.log 2>&1' | sudo tee /etc/cron.d/lab28-disk-report
ls -l /etc/cron.d/You should see something like this:
*/5 * * * * root /usr/local/bin/disk-report.sh >> /var/log/disk-report.log 2>&1
total 8
-rw-r--r--. 1 root root 128 Jan 23 2025 0hourly
-rw-r--r--. 1 root root 80 Oct 27 12:16 lab28-disk-reportA file in /etc/cron.d has the extra user field, here root, between the time and the command. It needs no crontab command and no restart, and it should be owned by root, mode 644, with no dots in its name. The script is called with its full path, so the short PATH doesn’t matter.
Wait until the next five-minute mark, then check:
sudo tail -n 3 /var/log/disk-report.logYou should see something like this:
/boot xfs 960M 297M 664M 31%
/boot/efi vfat 599M 8.4M 591M 2%
OK: every file system is below 80%✅ Checkpoint: /var/log/disk-report.log has a fresh report from cron.
5See how cron.daily runs through anacron on Rocky
Run this:
cat /etc/cron.d/0hourly
ls /etc/cron.hourly/
grep -v '^#' /etc/anacrontab | grep .
sudo cat /var/spool/anacron/cron.dailyYou should see something like this:
# Run the hourly jobs
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
01 * * * * root run-parts /etc/cron.hourly
0anacron
SHELL=/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
RANDOM_DELAY=45
START_HOURS_RANGE=3-22
1 5 cron.daily nice run-parts /etc/cron.daily
7 25 cron.weekly nice run-parts /etc/cron.weekly
@monthly 45 cron.monthly nice run-parts /etc/cron.monthly
20261027Read it as a chain: crond runs everything in /etc/cron.hourly at minute 01, 0anacron there starts anacron, and anacron runs cron.daily once a day (period 1) after 5 minutes plus up to 45 random minutes, only between 03:00 and 22:00. The timestamp file says cron.daily last ran today. If rocky01 is off for a week, the daily jobs run once, soon after it’s back on.
| On Rocky Linux / RHEL | On Ubuntu |
|---|---|
cron.daily, weekly and monthly go through anacron from /etc/cron.hourly/0anacron. Logs: journalctl -u crond and /var/log/cron | /etc/crontab runs cron.daily at 06:25 with run-parts, unless the anacron package is installed (it isn’t on a default Ubuntu Server). Logs: journalctl -u cron |
✅ Checkpoint: You can trace cron.daily from 0hourly to anacrontab and read its last-run date.
6Queue one-off jobs with at
Install at and start its daemon (on ubu01: sudo apt install at, which starts atd for you):
sudo dnf install -y at
sudo systemctl enable --now atdThen queue two jobs, list them and remove one:
echo '/usr/local/bin/disk-report.sh > /home/student/lab28/at-report.txt 2>&1' | at now + 2 minutes
echo 'echo late job' | at 23:30
atq
atrm 2
atqYou should see something like this:
warning: commands will be executed using /bin/sh
job 1 at Tue Oct 27 12:24:00 2026
warning: commands will be executed using /bin/sh
job 2 at Tue Oct 27 23:30:00 2026
1 Tue Oct 27 12:24:00 2026 a student
2 Tue Oct 27 23:30:00 2026 a student
1 Tue Oct 27 12:24:00 2026 a studentThe warning is normal: at always runs jobs with /bin/sh. “now + 2 minutes” is rounded to the start of a minute. Each atq line shows the job number, the time, the queue (a) and the owner.
After 12:24, head -n 2 ~/lab28/at-report.txt shows the report and job 1 is gone from atq. Unlike cron, at copies your current environment, PATH included, into the job; at -c 1 prints the whole script it will run.
If at prints “Can’t open /run/atd.pid to signal atd. No atd running?”, the job is queued but atd isn’t running, so nothing will ever run it. Start atd with sudo systemctl enable --now atd.
✅ Checkpoint: Job 1 ran and wrote at-report.txt, and job 2 is gone after atrm.
7Compare with the systemd timers on the same server
Run this:
systemctl list-timers --all --no-pager | head -n 5You should see something like this:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2026-10-27 12:51:16 IST 31min Tue 2026-10-27 11:43:52 IST 36min ago dnf-makecache.timer dnf-makecache.service
Wed 2026-10-28 00:00:00 IST 11h Tue 2026-10-27 09:12:04 IST 3h 7min ago logrotate.timer logrotate.service
Wed 2026-10-28 02:30:00 IST 14h Tue 2026-10-27 11:50:37 IST 29min ago disk-report.timer disk-report.service
Wed 2026-10-28 09:16:33 IST 20h Tue 2026-10-27 09:16:33 IST 3h 3min ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.servicerocky01 already schedules most of its own housekeeping with timers, including logrotate and disk-report from Lesson 27, and each line shows the next run and the last one, which cron can’t tell you. Right now disk-report runs twice: from its timer every night and from your /etc/cron.d file every five minutes. That’s a real problem on many servers, where a job was moved to a timer and the old cron line was never removed.
You’ll delete the cron copy at the end of the Break-It section.
Try it yourself: build the timer version of any cron job in the Command Builder.
✅ Checkpoint: You can see disk-report.timer in the list and you know the cron.d file is a duplicate.
Break It, Then Fix It
These two mistakes cause most “it works when I run it” tickets about cron. Both fail without any message on screen, so the skill here is finding the evidence. Keep the stamp job from the lab running while you do this.
1The script works by hand but not from cron (PATH)
Write a script that calls disk-report.sh by name, test it, then schedule it:
cat > ~/lab28/report.sh <<'EOF'
#!/bin/bash
# report.sh: save the latest disk report in the lab folder
disk-report.sh > /home/student/lab28/report-latest.txt
EOF
chmod 755 ~/lab28/report.sh
~/lab28/report.sh && head -n 1 ~/lab28/report-latest.txt
(crontab -l; echo '*/2 * * * * /home/student/lab28/report.sh >> /home/student/lab28/report.log 2>&1') | crontab -By hand it works:
Disk report for rocky01 at 2026-10-27 12:30:11Wait two minutes, then look at the job’s log:
cat ~/lab28/report.log
wc -c ~/lab28/report-latest.txtYou’ll get this:
/home/student/lab28/report.sh: line 3: disk-report.sh: command not found
0 /home/student/lab28/report-latest.txtYour login shell finds disk-report.sh because /usr/local/bin is in your PATH. cron’s PATH (you saw it in stamp.log) doesn’t have it, so bash can’t find the command and the redirect leaves an empty report file behind. Without the 2>&1 in the crontab line, that error would have gone to mail and been lost.
Fix: use full paths in scripts that cron runs (or set PATH at the top of the script).
sed -i 's|^disk-report.sh|/usr/local/bin/disk-report.sh|' ~/lab28/report.sh
sleep 120; tail -n 1 ~/lab28/report.log; head -n 1 ~/lab28/report-latest.txtYou should see something like this (no new error, and a fresh report):
/home/student/lab28/report.sh: line 3: disk-report.sh: command not found
Disk report for rocky01 at 2026-10-27 12:34:01The old error line is still the last line of report.log, but report-latest.txt is now filled at 12:34 by cron. You could also put PATH=/usr/local/bin:/usr/bin:/bin as the first line of the crontab. On Ubuntu 24.04 and later this example works even without the fix, because cron there inherits systemd’s PATH, but a script that relies on ~/bin or a variable from your .bashrc still breaks the same way.
✅ Checkpoint: report-latest.txt has content written by cron, not by you.
2A % sign cuts the command in half
Add a job that logs the time with date:
(crontab -l; echo '*/2 * * * * echo "ran at $(date +%H:%M)" >> /home/student/lab28/when.log') | crontab -Wait two minutes, then look for the log:
cat ~/lab28/when.logYou’ll get this:
cat: /home/student/lab28/when.log: No such file or directoryThe cron log shows what cron ran:
sudo journalctl -u crond --since '-3min' --no-pager | grep 'ran at'You should see something like this:
Oct 27 12:36:01 rocky01 CROND[2801]: (student) CMD (echo "ran at $(date +)
Oct 27 12:36:01 rocky01 CROND[2799]: (student) CMDEND (echo "ran at $(date +)cron cut the command at the first %, so the shell got echo "ran at $(date + with no closing bracket and no redirect. Run that half by hand and you see the error cron mailed to nobody:
sh -c 'echo "ran at $(date +'sh: -c: line 2: unexpected EOF while looking for matching `)'Fix: escape every % in the crontab line (or move the command into a script).
crontab -l | grep -v 'ran at' | crontab -
(crontab -l; echo '*/2 * * * * echo "ran at $(date +\%H:\%M)" >> /home/student/lab28/when.log') | crontab -
sleep 120; cat ~/lab28/when.logYou should see something like this:
ran at 12:40✅ Checkpoint: when.log exists and shows the time the job ran.
3Clean up
Remove your crontab, the duplicate system job and its log:
crontab -r
crontab -l
sudo rm /etc/cron.d/lab28-disk-report /var/log/disk-report.log
atqYou should see this:
no crontab for studentatq prints nothing because the queue is empty. disk-report.timer from Lesson 27 stays as the only schedule for the report. Keep ~/lab28 if you want to practise, or delete it.
✅ Checkpoint: No crontab, no lab file in /etc/cron.d, and disk-report.timer still listed by systemctl list-timers.
This happened at work
The NAV file that never left
This happened to me at a mutual fund house in Jaipur. Every weekday evening a job uploaded the day’s NAV file to the registrar, and the deadline was late in the evening. A developer had moved the job to a new server that afternoon, tested the command by pasting it into his shell, and added it to the crontab for 21:30.
At 22:10 the operations team called: the distributor portal still showed yesterday’s NAVs. The crontab line was there, crond was running, and the script ran perfectly when I started it by hand, so I uploaded the file manually at 22:25 and we made the deadline with minutes to spare. Nothing in the job’s log file had changed since the afternoon test.
The next morning I looked at the cron log instead of the job’s log. The CMD line for 21:30 ended at NAV_$(date +. The crontab line built the file name with date +%d%m%Y, cron had cut it at the first %, and the half-command failed with a shell syntax error.
The redirect to the log file was in the part cron threw away, and the error was mailed to a local account nobody read.
We moved the date logic into the script so the crontab line has no % at all, set MAILTO to the team mailbox through the company mail relay, and added a check at 22:00 that alerts if the file isn’t on the registrar’s side. Six months later we moved the job to a systemd timer with an OnFailure= alert, and the rule “never test a cron job by pasting it into your own shell” went into the team’s checklist.
Common mistakes and troubleshooting
| Mistake | What happens | What to do instead |
|---|---|---|
| Calling commands by name in a cron script | “command not found” under cron’s short PATH, though it works by hand | Use full paths, or set PATH at the top of the script or crontab |
Unescaped % in a crontab line, often in date +%F | The command is cut at the %, the redirect is lost and nothing is logged | Write \%, or move the command into a script |
| No redirect and no MAILTO on a server without mail | Errors vanish and the job “silently does nothing” | End every line with >> file.log 2>&1 or pipe to logger -t name |
| Leaving out the user field in /etc/cron.d or /etc/crontab | cron reads the command name as the user and the job never runs | Lines there are: five fields, user, command |
| A dot in a /etc/cron.d file name such as backup.cron | Ubuntu’s cron ignores the file without any warning | Use letters, digits, – and _ only |
Running crontab -r when you meant crontab -e | Your whole crontab is deleted with no question asked | Keep a copy: crontab -l > ~/crontab.bak before changes |
| Moving a job to a timer and keeping the old cron line | The job runs twice, sometimes at the same moment | Remove the cron line and check with grep -r name /etc/cron* /var/spool/cron |
Interview questions
Click a question to open it. Each one is written the way an interviewer would ask it. Under it you will see why they ask it and one way you could answer, in your own words.
Fresher level
1. Write a crontab line that runs /usr/local/bin/cleanup.sh at 11:45 p.m. every weekday, and tell me where its output goes.
Why they ask this: A basic syntax check. They also want to hear that you think about output, not only the timing fields.
How you might answer: It’s 45 23 * * 1-5 /usr/local/bin/cleanup.sh >> /var/log/cleanup.log 2>&1. The fields are minute, hour, day of month, month and day of week, and 1-5 is Monday to Friday.
Without the redirect the output is mailed to the crontab owner, and on most servers there’s no mail setup, so I always send stdout and stderr to a log file or to logger.
2. What is the difference between crontab -e and editing /etc/crontab?
Why they ask this: They’re checking that you know user crontabs and system crontabs are different formats and owned by different people.
How you might answer: crontab -e edits my own crontab, checks the syntax when I save and runs the jobs as me, so there’s no user field. /etc/crontab and files in /etc/cron.d are system crontabs with an extra user field after the five time fields.
For system jobs I prefer one file per job in /etc/cron.d, because a package update can replace /etc/crontab and a separate file is easier to review and remove.
2–5 years experience
3. A script runs fine when you start it, but does nothing from cron. How do you troubleshoot?
Why they ask this: A classic. They want a methodical answer that covers environment, logging and permissions.
How you might answer: First I check that cron ran it at all: journalctl -u crond or /var/log/cron shows a CMD line for each run, and on Ubuntu journalctl -u cron. If the line is there, I make sure the job logs its output with 2>&1 so I can see the error.
The usual causes are PATH (commands not found, because cron’s PATH has no /usr/local/bin on RHEL), a % in the crontab line, relative paths because cron starts in HOME, and variables that only exist in my .bashrc. I test with env -i HOME=$HOME PATH=/usr/bin:/bin /bin/sh -c '...' to get close to cron’s environment.
4. On Rocky, cron.daily jobs run at a different time every day. Why, and how would you control it?
Why they ask this: They want to know whether you understand anacron, not only cron.
How you might answer: On Rocky cron.daily isn’t run by cron directly. /etc/cron.d/0hourly runs cron.hourly, where 0anacron starts anacron, and anacron runs cron.daily with a 5-minute delay plus RANDOM_DELAY up to 45 minutes, within START_HOURS_RANGE 3-22.
If a job needs a fixed time, I don’t put it in cron.daily. I give it its own line in /etc/cron.d, or better a systemd timer with OnCalendar= and Persistent=true. I could also change /etc/anacrontab, but that affects every daily job.
Senior level
5. You inherit 150 servers with years of crontab entries. How would you get them under control?
Why they ask this: Seniors are expected to think about inventory, ownership and gradual migration, not a big rewrite.
How you might answer: First an inventory: collect every user crontab, /etc/crontab, /etc/cron.d and the cron.daily folders from all servers with Ansible, and put them in one sheet with owner, schedule and what the job does. Many will be dead jobs pointing at scripts that no longer exist.
Then I’d remove the dead ones with their owners’ agreement, move the important ones to systemd timers or a central scheduler with logging and alerts, and manage what stays as /etc/cron.d files from Ansible so the servers match what’s in git.
6. When would you still choose cron over a systemd timer today?
Why they ask this: They want judgement and honesty about trade-offs, not loyalty to one tool.
How you might answer: cron is fine for simple, low-risk jobs where a one-line schedule is clearer than two unit files, when other teams or vendor software expect to manage a crontab, or on systems where people only know cron and the job must be easy to find.
For anything that matters, I prefer timers: the journal keeps every run, OnFailure= can alert, Persistent=true catches up after downtime, and list-timers shows the next and last run. at stays my tool for one-off jobs.
Quick quiz
Q1. What does 0 9 * * 1-5 mean in a crontab?
- 9 minutes past every hour
- 09:00 Monday to Friday
- 09:00 on the 1st to 5th of each month
- Every 9 hours on weekdays
Show answer
B — Fields are minute, hour, day of month, month, day of week; 1-5 is Monday to Friday.
Q2. A crontab line uses date +%F and the job never writes its log. Why?
- cron doesn’t allow the date command
- cron cuts the command at the first unescaped %, so the rest, including the redirect, is lost
- The log folder is full
- %F only works in bash
Show answer
B — Escape it as \% or move the command into a script.
Q3. Which line is valid in /etc/cron.d/backup?
30 2 * * * /usr/local/bin/backup.sh30 2 * * * root /usr/local/bin/backup.shroot 30 2 * * * /usr/local/bin/backup.sh@daily backup.sh root
Show answer
B — System crontabs need the user field after the five time fields.
Q4. On Rocky Linux, what runs the scripts in /etc/cron.daily?
- crond directly at midnight
- anacron, started from /etc/cron.hourly/0anacron
- systemd-tmpfiles
- logrotate
Show answer
B — 0hourly runs cron.hourly, 0anacron starts anacron, and anacron runs cron.daily once a day within START_HOURS_RANGE.
Q5. Which command lists your pending at jobs?
at -rcrontab -latqsystemctl list-timers
Show answer
C — atq lists the queue (at -l is the same thing on both distributions); atrm removes a job.
Quick recap
- A crontab line is minute, hour, day of month, month, day of week, then the command; system crontabs add a user field.
- Use
crontab -eandcrontab -lfor your own jobs and one file per job in /etc/cron.d for system jobs, with no dots in the name. - Cron jobs get a small environment: use full paths, escape % as \%, and send output to a log with
2>&1. - On Rocky, anacron runs cron.daily, weekly and monthly from /etc/cron.hourly/0anacron; on Ubuntu /etc/crontab runs them at 06:25.
at,atqandatrmhandle one-off jobs, and atd must be running.- Find evidence in the cron log (
journalctl -u crondor-u cron), and prefer a systemd timer for jobs that need logging, alerts or catch-up.
Next lesson
Lesson 29: Logging: journald, rsyslog, remote syslog and logrotate → You’ll make the journal persistent, send rocky01’s logs to ubu01 with rsyslog, and write a logrotate rule you can test before it runs.
Further reading: Linux cron jobs
Want to go deeper into Linux cron jobs? Read the man pages for crontab(5), crontab(1), anacron(8) and anacrontab(5), Debian’s at(1) page, and the cronie project that Rocky Linux uses. You can also see the full plan for this course on the undefined course page.
