Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Schedule tasks with cron and crontab on Linux

By updated

Improve this page

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:

  1. 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
  2. The /etc/crontab file, the system crontab
  3. The files in /etc/cron.d/, additional crontabs dropped by packages
  4. The /etc/cron.{hourly,daily,weekly,monthly}/ directories, scripts run by run-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:

Terminal window
# Debian/Ubuntu
systemctl status cron
# RHEL/Fedora/Rocky/Alma
systemctl 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 run

Operators

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.

OperatorMeaningExample
*Every value* * * * * = every minute
,List of values1,15 * * * * = at minutes 1 and 15
-Range1-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 * * *.

ExpressionMeaning
0 3 * * *Every day at 3:00
30 8 * * 1-5Monday 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 * * 0Every Sunday at 22:00

Shortcuts

cron also recognises shortcuts that replace the 5 fields, easier to read for the most common schedules:

ShortcutEquivalentMeaning
@reboot-At system startup
@hourly0 * * * *Every hour
@daily0 0 * * *Every day at midnight
@weekly0 0 * * 0Every Sunday at midnight
@monthly0 0 1 * *The 1st of the month at midnight
@yearly0 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.

Terminal window
crontab -e

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

Terminal window
crontab -l

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

Terminal window
crontab -r

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

Terminal window
sudo crontab -u www-data -e

System 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 command
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 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.

/etc/cron.d/0hourly
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.daily
7 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.

Terminal window
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.

DirectoryFrequency
/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.

Terminal window
# Define variables at the top of the crontab
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=admin@example.com
# The command uses the full PATH
0 3 * * * /usr/local/bin/backup.sh
VariableEffect
SHELLShell used to run the commands
PATHDirectories searched for commands
MAILTORecipient of the output (empty = no e-mail)
HOMEWorking 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.

FileEffect
/etc/cron.allowIf this file exists, only the listed users may use cron
/etc/cron.denyThe 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:

Terminal window
# Redirect stdout and stderr to a log file
0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# Discard all output
0 3 * * * /usr/local/bin/backup.sh > /dev/null 2>&1
# Redirect stderr only to a file
0 3 * * * /usr/local/bin/backup.sh 2>> /var/log/backup-errors.log

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

Terminal window
# Back up /home every night at 2:30
30 2 * * * /usr/bin/tar czf /backup/home-$(date +\%Y\%m\%d).tar.gz /home

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

Terminal window
# Delete files older than 7 days in /tmp every day at 4:00
0 4 * * * /usr/bin/find /tmp -type f -mtime +7 -delete

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

Terminal window
# Alert if the disk exceeds 90 %, every hour
0 * * * * df -h / | awk 'NR==2 && int($5)>90 {print "DISK ALERT: "$5}' | mail -s "Disk full" admin@example.com

Troubleshooting

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.

SymptomLikely causeSolution
Job does not runInsufficient PATHUse absolute paths or set PATH at the top of the crontab
date prints %Y%m%dUnescaped %Replace % with \% in the crontab
No visible outputOutput not redirectedAdd >> /var/log/job.log 2>&1
Permission deniedScript not executablechmod +x /path/to/script.sh
File in /etc/cron.d/ or cron.daily/ never runName containing a dot (Debian/Ubuntu)Rename without a dot, check with run-parts --test
Job does not run (user)Blocked by cron.denyCheck /etc/cron.allow and /etc/cron.deny
cron service inactiveDaemon stoppedsudo systemctl enable --now cron (Debian/Ubuntu) or sudo systemctl enable --now crond (RHEL-like)
Script works in the CLI but not in cronMissing environment variablesSet 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.

Terminal window
# Debian/Ubuntu
journalctl -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 installed
grep CRON /var/log/syslog | tail -20
# RHEL-like with rsyslog (dedicated file)
grep CROND /var/log/cron | tail -20

Best 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 flock to prevent parallel runs:
Terminal window
# Prevent concurrent runs with flock
*/5 * * * * /usr/bin/flock -n /tmp/backup.lock /usr/local/bin/backup.sh
  • Restrict access, use /etc/cron.allow to 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 -e edits the crontab of the current user, crontab -l displays it, crontab -r deletes it without confirmation (crontab -i -r asks)
  • 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/crontab holds no job: cron.hourly goes through /etc/cron.d/0hourly, the rest through anacron
  • /etc/cron.allow and /etc/cron.deny control access to cron; RHEL ships an empty cron.deny that allows everyone
  • Always handle the output (redirection or MAILTO) and lock long jobs with flock

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

6 questions
6 min
70% to pass

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

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn