cron is the historical task scheduler of Linux. It runs commands at defined intervals, every minute, every day at 3 am, every Monday, following a 5-field syntax. It is the basic tool to automate backups, file cleanup, synchronisations and any recurring operation on a server.
What you will learn
- Understand how the cron daemon works and its configuration files
- Read and write the 5-field syntax of a crontab line
- Create, modify and delete user crontabs with
crontab -e - Use the system directories
/etc/cron.d/,/etc/cron.daily/and so on - Diagnose a cron job that does not run
- Apply the security and reliability best practices
In which context?
cron comes into play as soon as an action must repeat automatically on a Linux system:
- Back up a database or a directory every night
- Clean up temporary files or obsolete logs
- Synchronise data between servers with
rsync - Renew a Let's Encrypt certificate
- Send a report by e-mail every week
- Check the disk space and alert when a threshold is exceeded
How cron works
The cron daemon (crond on Red Hat, cron on Debian/Ubuntu) runs permanently. Every minute, it checks:
- The user crontabs, edited with
crontab -e, stored in/var/spool/cron/crontabs/on Debian and Ubuntu, directly in/var/spool/cron/on RHEL and AlmaLinux - The
/etc/crontabfile, the system crontab - The files in
/etc/cron.d/, additional crontabs dropped by packages - The
/etc/cron.{hourly,daily,weekly,monthly}/directories, scripts run byrun-parts
The two families do not trigger these directories the same way. On Debian and Ubuntu, /etc/crontab calls run-parts. On RHEL and AlmaLinux, /etc/crontab only holds variables: the /etc/cron.d/0hourly file starts cron.hourly, and anacron, a scheduler that catches up with jobs missed while the machine was off, handles the daily, weekly and monthly directories. The service name differs too:
# Debian/Ubuntusystemctl status cron
# RHEL/Fedora/Rocky/Almasystemctl status crond● cron.service - Regular background program processing daemon Loaded: loaded (/usr/lib/systemd/system/cron.service; enabled; preset: enabled) Active: active (running)The crontab syntax
A crontab is a text file where each uncommented line describes a job: five time fields, then the command to run. The daemon compares these five fields with the clock once a minute, and runs the command when they all match. The minute is therefore the finest possible unit, cron cannot fire every ten seconds.
Each line of a crontab follows this format:
┌───────────── minute (0-59)│ ┌───────────── hour (0-23)│ │ ┌───────────── day of month (1-31)│ │ │ ┌───────────── month (1-12)│ │ │ │ ┌───────────── day of week (0-7, 0 and 7 = Sunday)│ │ │ │ │* * * * * command to runOperators
Each of the five fields accepts the same operators, which combine with each other (0,30 8-18 * * 1-5 is valid). Remember above all that the fields are joined by a logical AND, except the day of month and the day of week: when both are set at the same time, cron runs the job as soon as either one matches. This is the source of surprise runs, put * in one of the two unless you need this behaviour.
| Operator | Meaning | Example |
|---|---|---|
* | Every value | * * * * * = every minute |
, | List of values | 1,15 * * * * = at minutes 1 and 15 |
- | Range | 1-5 * * * * = minutes 1 to 5 |
/ | Step (interval) | */10 * * * * = every 10 minutes |
Practical examples
Always read an expression from left to right, starting with the minute. The most common mistake is to write * 3 * * * meaning "at 3 am": this line runs the command sixty times, every minute between 3:00 and 3:59. For a single run at a given hour, the minute field must hold a precise value, here 0 3 * * *.
| Expression | Meaning |
|---|---|
0 3 * * * | Every day at 3:00 |
30 8 * * 1-5 | Monday to Friday at 8:30 |
0 */2 * * * | Every 2 hours (at minute 0) |
0 0 1 * * | On the 1st of every month at midnight |
*/5 * * * * | Every 5 minutes |
0 22 * * 0 | Every Sunday at 22:00 |
Shortcuts
cron also recognises shortcuts that replace the 5 fields, easier to read for the most common schedules:
| Shortcut | Equivalent | Meaning |
|---|---|---|
@reboot | - | At system startup |
@hourly | 0 * * * * | Every hour |
@daily | 0 0 * * * | Every day at midnight |
@weekly | 0 0 * * 0 | Every Sunday at midnight |
@monthly | 0 0 1 * * | The 1st of the month at midnight |
@yearly | 0 0 1 1 * | January 1st at midnight |
Manage user crontabs
Every account on the system has its own crontab, a file you never edit directly. Its location depends on the distribution: /var/spool/cron/crontabs/<user> on Debian and Ubuntu, /var/spool/cron/<user> on RHEL and AlmaLinux, where the files sit directly in /var/spool/cron/. On RHEL, this directory also carries the SELinux context user_cron_spool_t (read from the targeted policy of AlmaLinux 10): a file copied by hand does not necessarily inherit the right context. So you go through the crontab command, which checks the syntax before saving and notifies the daemon of the change. The jobs run under the identity of the file owner, with its rights and nothing more.
Edit the crontab
crontab -e opens a temporary copy of the file in an editor, then reinstalls it on save after syntax validation. If a line is malformed, the command refuses to save it and offers to fix it, the previous crontab stays in place. With the hour set to 25, the measured refusal names the line and the wrong field: bad hour, followed by errors in crontab file, can't install on Debian and Ubuntu, or by Invalid crontab file, can't install on AlmaLinux 10.
crontab -eThe default editor opens, set by the $EDITOR or $VISUAL variable. Add your lines and save.
Display the current crontab
crontab -l writes the content of the file on standard output, without opening an editor. It is the command to check what is actually scheduled, and to make a backup to a file before any change.
crontab -lDelete the whole crontab
crontab -r erases the entire crontab of the current user, immediately and without asking for confirmation. There is no recycle bin and no undo: the file disappears from the spool directory and the jobs stop being scheduled from the next minute.
crontab -rEdit another user's crontab (root)
The -u option names the target account and can only be used with root rights. It is the right way to schedule an application job: it runs under the service account (www-data, postgres) instead of root, which limits the damage if the script goes wrong. The account concerned can then keep editing the file with a plain crontab -e.
sudo crontab -u www-data -eSystem files
Next to personal crontabs, the system has its own locations, fed by the administrator and by the installed packages. Two differences set them apart from a user crontab: they carry an extra user field, and they are edited as ordinary files, hence without the syntax check of crontab -e. A typo goes unnoticed there until the job fails to fire.
/etc/crontab
The system crontab contains an extra user field, placed between the five time fields and the command. Here is its content on Debian 13 and Ubuntu 24.04, read without the comments:
# m h dom mon dow user command17 * * * * root cd / && run-parts --report /etc/cron.hourly25 6 * * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.daily; }47 6 * * 7 root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.weekly; }52 6 1 * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.monthly; }The test -x /usr/sbin/anacron || works like this: if anacron is installed, it takes over the daily, weekly and monthly directories, and cron steps aside so as not to run them twice. On AlmaLinux 10, the same file only contains SHELL, PATH and MAILTO: the periodic jobs go through /etc/cron.d/0hourly then /etc/anacrontab, shown below.
01 * * * * root run-parts /etc/cron.hourly
# /etc/anacrontab (period in days, delay in minutes, identifier, command)1 5 cron.daily nice run-parts /etc/cron.daily7 25 cron.weekly nice run-parts /etc/cron.weekly@monthly 45 cron.monthly nice run-parts /etc/cron.monthly/etc/cron.d/
This directory holds files in the system crontab format, hence with the user field. Packages drop their scheduled jobs there, and its content depends on what is installed: an Ubuntu 24.04 image holds e2scrub_all; AlmaLinux 10 holds 0hourly.
ls /etc/cron.d/On Debian and Ubuntu, the name of a file in /etc/cron.d/ may only contain letters, digits, hyphens and underscores: the man 8 cron manual of Debian 13 states that a file whose name contains a dot is ignored. Name it nightly-backup, never nightly.cron.
Periodic directories
The scripts placed in these directories are run automatically by run-parts, at the frequency given by the directory name. No crontab line to write: the file itself is the job.
| Directory | Frequency |
|---|---|
/etc/cron.hourly/ | Every hour |
/etc/cron.daily/ | Every day |
/etc/cron.weekly/ | Every week |
/etc/cron.monthly/ | Every month |
Environment variables
cron runs commands in a minimal environment. The PATH is reduced, and the variables of your interactive shell are not available.
# Define variables at the top of the crontabSHELL=/bin/bashPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binMAILTO=admin@example.com
# The command uses the full PATH0 3 * * * /usr/local/bin/backup.sh| Variable | Effect |
|---|---|
SHELL | Shell used to run the commands |
PATH | Directories searched for commands |
MAILTO | Recipient of the output (empty = no e-mail) |
HOME | Working directory (the user's by default) |
Access control
Two files control who may use cron: an allow list and a deny list, the first one taking precedence when it exists.
| File | Effect |
|---|---|
/etc/cron.allow | If this file exists, only the listed users may use cron |
/etc/cron.deny | The listed users are denied (ignored if cron.allow exists) |
If neither file exists, the behaviour depends on the distribution. Measured with an ordinary account:
- Debian/Ubuntu: every user may use cron, and this is the default situation, since neither file is shipped
- RHEL/AlmaLinux: only root may use cron, the others get
You (testeur) are not allowed to use this program (crontab)
On AlmaLinux 10, this second case only happens after a deletion, though: the cronie package ships an empty /etc/cron.deny, which allows everyone. Deleting that empty file while tidying up therefore closes cron to every account except root.
Output redirection
By default, cron sends the standard and error output by e-mail (through MAILTO). To redirect it to a file or discard it:
# Redirect stdout and stderr to a log file0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# Discard all output0 3 * * * /usr/local/bin/backup.sh > /dev/null 2>&1
# Redirect stderr only to a file0 3 * * * /usr/local/bin/backup.sh 2>> /var/log/backup-errors.logConcrete examples
The three jobs below cover the recurring needs of a server: produce a backup, recover disk space, and alert before the disk is full. Each one also shows a constraint specific to cron: escaping the %, using absolute paths, and depending on a working mail service on the machine.
Daily backup
This line dates the archive with the day it runs, provided the % is escaped. Note the full path /usr/bin/tar: in the reduced environment of cron, a plain tar may not be found. Also check that /backup exists and that the owner of the crontab may write there, otherwise the job will fail silently every night.
# Back up /home every night at 2:3030 2 * * * /usr/bin/tar czf /backup/home-$(date +\%Y\%m\%d).tar.gz /homeTemporary files cleanup
-mtime +7 selects the files whose content has not been modified for eight days or more: find rounds the age down to the day before comparing, so a file seven and a half days old is not selected (measured with touch -d). A file that was merely read since is not spared. -type f restricts to ordinary files, which avoids hitting directories and sockets that active services still use. Always test the command without -delete first: the list it prints tells you exactly what will disappear.
# Delete files older than 7 days in /tmp every day at 4:000 4 * * * /usr/bin/find /tmp -type f -mtime +7 -deleteDisk space check
awk skips the header line thanks to NR==2 and compares the fifth column, the usage percentage; int($5) strips the % character to allow a numeric comparison (checked with gawk and mawk). The mail command only sends a message when the condition is true, so a healthy server stays silent. It requires a mail transfer agent installed and configured locally, otherwise the alert is lost without a trace.
# Alert if the disk exceeds 90 %, every hour0 * * * * df -h / | awk 'NR==2 && int($5)>90 {print "DISK ALERT: "$5}' | mail -s "Disk full" admin@example.comTroubleshooting
A cron job that produces nothing always raises the same question: did the daemon try to run it, or did the command fail once started? The journal answers, because cron writes a line there at every trigger, with the user and the command. If this line exists, the problem is in the script or its environment; if it is missing, it is in the schedule or in the state of the service.
| Symptom | Likely cause | Solution |
|---|---|---|
| Job does not run | Insufficient PATH | Use absolute paths or set PATH at the top of the crontab |
date prints %Y%m%d | Unescaped % | Replace % with \% in the crontab |
| No visible output | Output not redirected | Add >> /var/log/job.log 2>&1 |
Permission denied | Script not executable | chmod +x /path/to/script.sh |
File in /etc/cron.d/ or cron.daily/ never run | Name containing a dot (Debian/Ubuntu) | Rename without a dot, check with run-parts --test |
| Job does not run (user) | Blocked by cron.deny | Check /etc/cron.allow and /etc/cron.deny |
| cron service inactive | Daemon stopped | sudo systemctl enable --now cron (Debian/Ubuntu) or sudo systemctl enable --now crond (RHEL-like) |
| Script works in the CLI but not in cron | Missing environment variables | Set SHELL, PATH, HOME at the top of the crontab |
The commands below query the journal depending on the distribution. The text files only exist when rsyslog is installed: it is the case by default on Ubuntu 24.04 (a package of important priority), not on Debian 13, where it moved to optional priority, so /var/log/syslog is missing from a fresh install.
# Debian/Ubuntujournalctl -u cron --since "1 hour ago"
# RHEL/Fedora/Rocky/Alma: filter on the process (see the box above)journalctl _COMM=crond --since "1 hour ago"
# Ubuntu, or Debian with rsyslog installedgrep CRON /var/log/syslog | tail -20
# RHEL-like with rsyslog (dedicated file)grep CROND /var/log/cron | tail -20Best practices
A scheduled job runs with nobody in front of the screen: it must therefore fail loudly rather than quietly. These rules target three concrete risks: the command not found for lack of a PATH, the error lost for lack of redirection, and runs that overlap when a job lasts longer than its interval.
- Absolute paths everywhere, do not assume a full
PATH - Handle the output, redirect to a log file or set
MAILTO - Escape the
%in crontabs, always\% - Comment the lines, explain each job with
# - Test by hand before adding a job, run the command as the cron user
- Lock long jobs, use
flockto prevent parallel runs:
# Prevent concurrent runs with flock*/5 * * * * /usr/bin/flock -n /tmp/backup.lock /usr/local/bin/backup.sh- Restrict access, use
/etc/cron.allowto allow legitimate users only
Put it into practice
The 5-field syntax only really sinks in once you see a job run by itself. This lab puts you on a VM where you write a crontab with crontab -e, schedule a recurring job and prove it ran by finding its traces in the cron journal.
Key points
- cron runs jobs at regular intervals following a 5-field syntax (minute, hour, day, month, day of week)
crontab -eedits the crontab of the current user,crontab -ldisplays it,crontab -rdeletes it without confirmation (crontab -i -rasks)- cron commands run in a minimal environment, always use absolute paths
- The
%must be escaped as\%in crontabs /etc/cron.d/and/etc/cron.{daily,weekly,monthly}/hold the system jobs; on Debian and Ubuntu, a file name containing a dot is ignored there- On RHEL,
/etc/crontabholds no job:cron.hourlygoes through/etc/cron.d/0hourly, the rest through anacron /etc/cron.allowand/etc/cron.denycontrol access to cron; RHEL ships an emptycron.denythat allows everyone- Always handle the output (redirection or
MAILTO) and lock long jobs withflock
Knowledge check
Check that the essentials of this guide stuck. The questions cover only what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Schedule a one-off job with at, atq and atrm
A new full quiz with random questions
Work again only on the questions you missed