Crontab: Scheduling Examples in Linux, Syntax and Common Mistakes
Crontab is a schedule table that the cron daemon uses to run commands: each line consists of five time fields (minute, hour, day of month, month, day of week) and a command, for example */5 * * * * /opt/backup.sh runs a script every 5 minutes. The table is edited with crontab -e, and most problems come from the stripped-down cron environment: use full paths and redirect the output to a log.
Cron is the standard task scheduler in Linux: the cron daemon reads schedule tables (crontabs) once a minute and runs the commands whose time has come. This is how nightly backups are made, temporary files are cleaned up, certificates are renewed and reports are sent. Below are the syntax, crontab examples for typical cases and an analysis of the mistakes that make a job fail "silently".
How to open and edit a crontab
Each user has their own table. It is worked with through the crontab command, rather than by editing the files in /var/spool/cron directly:
| Command | What it does |
|---|---|
crontab -e | open your table in an editor (on first run it asks you to choose an editor) |
crontab -l | show the current table |
crontab -r | delete the whole table, without confirmation |
crontab -i -r | delete with confirmation (Debian/Ubuntu, RHEL) |
sudo crontab -u alice -e | edit another user's table |
crontab file | replace the table with the contents of a file |
After saving, crontab -e checks the syntax and prints crontab: installing new crontab. If a line has an error, you will see errors in crontab file, can't install and an offer to edit again. There is no need to restart the daemon — changes are picked up automatically.
Be careful with -r: the letters e and r are next to each other on the keyboard, and there is no backup of the table. A useful habit is to keep a backup:
crontab -l > ~/crontab.bak
# restore
crontab ~/crontab.bak
Crontab syntax: five time fields
Each job line looks like this:
# ┌───────── minute (0–59)
# │ ┌─────── hour (0–23)
# │ │ ┌───── day of month (1–31)
# │ │ │ ┌─── month (1–12 or jan–dec)
# │ │ │ │ ┌─ day of week (0–7 or sun–sat; 0 and 7 are Sunday)
# │ │ │ │ │
* * * * * command
Each field can use special characters:
| Character | Meaning | Example |
|---|---|---|
* | any value | * * * * * — every minute |
, | list | 0 9,18 * * * — at 9:00 and 18:00 |
- | range | 0 9-18 * * * — every hour from 9 to 18 |
/ | step | */15 * * * * — every 15 minutes |
A step can be combined with a range: 0 8-20/2 * * * — every two hours from 8 to 20 (8, 10, …, 20). Time is taken from the system's time zone — check it with timedatectl.
Crontab examples for every day
| Schedule | Crontab line |
|---|---|
| every minute | * * * * * |
| every 5 minutes | */5 * * * * |
| every 30 minutes | */30 * * * * or 0,30 * * * * |
| every hour, on the hour | 0 * * * * |
| every 6 hours | 0 */6 * * * |
| every day at 3:00 | 0 3 * * * |
| every day at 23:45 | 45 23 * * * |
| on weekdays at 9:00 | 0 9 * * 1-5 |
| on weekends at noon | 0 12 * * 6,0 |
| on Mondays at 8:30 | 30 8 * * 1 |
| on the 1st of every month at 00:00 | 0 0 1 * * |
| once a quarter | 0 0 1 1,4,7,10 * |
| once a year, 1 January | 0 0 1 1 * |
| every 10 minutes during working hours | */10 9-18 * * 1-5 |
A full job line is the schedule plus the command:
# database backup every night at 3:00
0 3 * * * /usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1
# clean up temporary files older than 7 days on Sundays
0 4 * * 0 /usr/bin/find /tmp/app -type f -mtime +7 -delete
# check the site every 5 minutes
*/5 * * * * /usr/bin/curl -fsS https://example.com/health > /dev/null || /usr/local/bin/alert.sh
Special strings @reboot, @daily and others
Instead of five fields you can write a short alias:
| Alias | Equivalent |
|---|---|
@reboot | once when the cron daemon starts (after the system boots) |
@hourly | 0 * * * * |
@daily / @midnight | 0 0 * * * |
@weekly | 0 0 * * 0 |
@monthly | 0 0 1 * * |
@yearly / @annually | 0 0 1 1 * |
@reboot /home/alice/bin/start-tunnel.sh >> /home/alice/tunnel.log 2>&1
For services that must run permanently and restart after a crash, @reboot is a weak solution: it is better to write a systemd unit with Restart=on-failure.
How to run a job every N seconds
The smallest unit of cron is one minute. If you need it more often, there are two approaches. The first is several lines with a delay via sleep:
* * * * * /opt/check.sh
* * * * * sleep 30; /opt/check.sh
The second, more correct one is a systemd timer with OnUnitActiveSec=30s. If a task runs more than once a minute, it is usually a job for a daemon or a timer rather than for cron.
System crontab: /etc/crontab and /etc/cron.d
Besides user tables there are system ones. Their format is different: between the time and the command there is a user name, the user the command runs as.
# /etc/cron.d/certbot-renew
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
0 */12 * * * root certbot renew --quiet
What lives where:
/etc/crontab— the main system table; on Debian/Ubuntu the directories below are run from it./etc/cron.d/— separate files with jobs, convenient to install with packages or through Ansible./etc/cron.hourly/,cron.daily/,cron.weekly/,cron.monthly/— executable scripts without a schedule go here; they are run byrun-parts(or anacron).
A catch with run-parts: on Debian and Ubuntu it skips files whose names contain a dot. A script backup.sh in /etc/cron.daily/ will not run — name it backup. To see what will be executed, run run-parts --test /etc/cron.daily.
Files in /etc/cron.d/ must be owned by root, must not be writable by the group or others, and must end with a newline — otherwise cron ignores them.
The cron environment: PATH, SHELL and variables
The main reason behind "it works in the terminal, but not in cron" is the environment. Cron runs commands through /bin/sh, without your .bashrc and .profile, and with a minimal PATH (usually /usr/bin:/bin). That is why docker, node or php located in /usr/local/bin may simply not be found.
Variables can be set at the top of the table:
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=admin@example.com
CRON_TZ=Europe/Moscow
0 6 * * * cd /srv/app && ./report.sh
MAILTO— where to send the job's output;MAILTO=""disables the emails.CRON_TZ— the time zone for the schedule (supported by cronie in RHEL/Fedora; the Debian version of cron does not honour this variable).- Substitutions such as
$HOME/bininside variable values are not performed — write full paths.
The most reliable approach is to use absolute paths to all programs and files in the job, and to put a shebang #!/bin/bash on the first line of scripts.
Where to look at cron logs
Cron records in the log the fact that a job was started, but not its output:
# Debian/Ubuntu
grep CRON /var/log/syslog
# via systemd-journald
journalctl -u cron --since today # Debian/Ubuntu
journalctl -u crond --since today # RHEL, Fedora, Rocky
# RHEL without journald
tail -f /var/log/cron
A start line looks roughly like this:
Oct 14 03:00:01 web1 CRON[21544]: (root) CMD (/usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1)
To see what a job printed and with which error it failed, redirect both stdout and stderr to a file: >> /var/log/job.log 2>&1. Without this, the output is sent by email through the local mail system, and if no mail agent is installed it is lost (the log will show No MTA installed, discarding output).
Why cron does not run a job: common mistakes
Command not found
Symptom: command not found in the job's log. The cause is a stripped-down PATH. The fix: the full path to the program (which docker will tell you it) or a PATH= line at the top of the table.
A percent sign in the command
In a crontab the % character means a newline: everything after the first % is passed to the command on standard input. That is why the classic line with a date in a file name breaks:
# wrong — the command is cut off at the first %
0 2 * * * tar czf /backup/site-$(date +%F).tar.gz /var/www
# right — escape it
0 2 * * * tar czf /backup/site-$(date +\%F).tar.gz /var/www
Simpler still — move the logic into a script, where % does not need escaping.
The script is not executable or has the wrong line endings
The error Permission denied means the file has no x bit: run chmod +x script.sh or start it explicitly with /bin/bash /opt/script.sh. If the script was written on Windows, CRLF line endings produce a strange error /bin/bash^M: bad interpreter — fix it with sed -i 's/\r$//' script.sh or dos2unix.
Relative paths and the working directory
A job starts in the user's home directory. If the script reads ./config.ini, it will look for it in ~. Add cd /srv/app && before the command or use absolute paths inside the script.
Day of month and day of week at the same time
If both fields are set, cron runs when either of them matches, not both. The line 0 9 13 * 5 will run on the 13th and every Friday, not only on Friday the 13th. Check the required condition inside the command:
0 9 13 * * [ "$(date +\%u)" = 5 ] && /opt/friday13.sh
Jobs overlap each other
If a backup takes longer than the interval, the next run starts on top of the previous one. The protection is locking with flock:
*/10 * * * * /usr/bin/flock -n /tmp/sync.lock /opt/sync.sh
With the -n flag the second instance exits immediately if the first one is still running.
The cron daemon is not running
Check systemctl status cron (on RHEL it is crond). Minimal images, for example Docker containers, have no cron daemon at all — there is simply nothing there to run the crontab jobs.
How to debug a cron job
Run the command in an environment as close to cron's as possible:
env -i SHELL=/bin/sh PATH=/usr/bin:/bin HOME=$HOME /bin/sh -c '/opt/backup.sh'
If an error appears here, it will also appear in cron. Temporarily set the schedule to * * * * *, redirect the output to a file and follow it with tail -f. Online cron expression calculators help check the schedule itself, but the final word belongs to the system log.
Who can use cron: cron.allow and cron.deny
Access is restricted by the files /etc/cron.allow and /etc/cron.deny. If cron.allow exists, only the users listed in it may use crontab. If it does not exist but cron.deny does, everyone except those listed may use it. A user on the denied list who runs crontab -e gets the message You (bob) are not allowed to use this program.
Summary
A crontab is lines of five time fields followed by a command: */5 * * * * — every 5 minutes, 0 3 * * * — every night, 0 9 * * 1-5 — on weekdays. Edit the table with crontab -e, use absolute paths, escape %, write the output to a log and protect long jobs with flock. These crontab examples cover most server tasks, and automation and task scheduling are covered in practice in our Linux course.
FAQ
How is cron different from anacron?
Cron runs jobs at the exact time and skips them if the computer was switched off. Anacron guarantees that daily, weekly and monthly jobs run after the machine is turned on, which makes it suitable for laptops and desktops.
Which is better: cron or systemd timers?
Cron is simpler and available almost everywhere. systemd timers provide logs in journalctl, dependencies, resource limits and the Persistent=true option for missed runs, so on modern servers they are increasingly chosen for important tasks.
Where is a user's crontab physically stored?
On Debian and Ubuntu it is in /var/spool/cron/crontabs/name, on RHEL and Fedora in /var/spool/cron/name. It is not advisable to edit these files directly; use crontab -e.
How do I temporarily disable a job in crontab?
Comment out its line with the # character at the start via crontab -e. To disable all of a user's jobs, save the table to a file with crontab -l and then delete it with crontab -r.
How do I run a job on the last day of the month?
There is no separate field for this. Schedule the job for the 28th–31st and check that tomorrow is the 1st: 0 23 28-31 * * [ "$(date -d tomorrow +\%d)" = 01 ] && /opt/job.sh.
This topic is covered in depth with practice in the course “Linux: с нуля до уверенного администрирования”.
Go to course