cron Cheatsheet
Crontab Syntax
Use this cron reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
The five fields
A crontab line is five time fields, then the command. Fields are separated by whitespace, and the command is everything left on the line.
# ┌───────────── 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 both Sunday # │ │ │ │ │ * * * * * /usr/local/bin/job.sh
| Field | Range | Notes |
|---|---|---|
| minute | 0-59 | — |
| hour | 0-23 | 24-hour clock, no AM/PM, no 24 |
| day of month | 1-31 | a 31 in a 30-day month simply never fires |
| month | 1-12 | or jan feb … dec, case insensitive |
| day of week | 0-7 | or sun mon … sat, both 0 and 7 are Sunday |
There is no seconds field and no year field. The smallest interval cron can express is one minute, and cron wakes up once a minute to check.
0 3 * * * /usr/local/bin/backup.sh # 03:00 daily 30 8 * * 1 /usr/local/bin/weekly.sh # Mondays at 08:30 0 0 1 * * /usr/local/bin/monthly.sh # midnight, first of the month */15 * * * * /usr/local/bin/poll.sh # :00 :15 :30 :45 0 9-17 * * 1-5 /usr/local/bin/business.sh # hourly, 9am-5pm, weekdays
Operators inside a field
| Operator | Meaning | Example |
|---|---|---|
* | every value in the range | * * * * * every minute |
, | a list | 0,15,30,45 |
- | an inclusive range | 9-17 is 9 through 17 |
/ | a step through a range | */5, 0-30/10 |
| names | month and weekday only | jan, mon |
*/5 is shorthand for "the whole range, every fifth value", so in the minute
field it means 0, 5, 10, … 55. A step applies to the range it is attached to, so
0-30/10 is 0, 10, 20, 30 and nothing after 30.
Lists and ranges mix freely in Vixie cron and cronie: 1-3,7-9 is valid, even
though the original AT&T and BSD crons rejected it. Name ranges and name lists
(mon-wed, mon,wed,fri) work in cronie but not in the BSD and macOS cron,
whose man page says ranges and lists of names are not allowed. Use numbers when
portability matters.
The day-of-month versus day-of-week trap
This is the single most misread rule in cron. When both day fields are
restricted (neither is *), cron runs the job when either matches. The two
day fields are OR'd, not AND'd, while every other field is AND'd.
30 4 1,15 * 5 /usr/local/bin/job.sh
That runs at 04:30 on the 1st and the 15th of every month, plus every Friday. It is not "the 1st or 15th, but only if it is a Friday".
| Day of month | Day of week | Behavior |
|---|---|---|
* | * | every day |
15 | * | the 15th only |
* | 5 | Fridays only |
15 | 5 | the 15th or any Friday |
To get an actual AND, restrict one field and test the other in the shell.
# first Monday of the month: days 1-7, and only if it is a Monday 0 3 1-7 * * [ "$(date +\%u)" = 1 ] && /usr/local/bin/job.sh # last day of the month: run daily, bail unless tomorrow is the 1st 0 23 28-31 * * [ "$(date -d tomorrow +\%d)" = 01 ] && /usr/local/bin/job.sh # (GNU date. On BSD and macOS the equivalent is date -v+1d +\%d)
Note the escaped \%, which the next section explains.
% must be escaped
In the command field, an unescaped % is turned into a newline, and everything
after the first % is fed to the command as standard input. That is a Vixie cron
feature almost nobody wants, and it silently mangles every date +%Y format
string.
# Broken: cron cuts the line at the first % 0 3 * * * tar czf /backup/db-$(date +%F).tgz /var/lib/db # Correct: escape every percent with a backslash 0 3 * * * tar czf /backup/db-$(date +\%F).tgz /var/lib/db
The intended use, sending stdin to a command:
0 22 * * 1-5 mail -s "It is 10pm" joe%Do you know where your kids are?
The reliable fix is to put anything nontrivial in a script and call the script,
so there is no % on the crontab line at all.
Environment lines and comments
Above the schedule lines a crontab may set environment variables, one per line.
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=ops@example.com
HOME=/home/deploy
CRON_TZ=UTCThe rules that trip people up:
- name = value with spaces around = is fine, and any non-leading space inside
value is part of the value. Quote to keep leading or trailing spaces.
- A comment cannot share a line with anything else. # must be the first
non-space character on its own line. A trailing # note after a command
becomes part of the command.
- No shell expansion happens on the right-hand side. PATH=$PATH:/opt/bin sets
the literal string, it does not append.
- LOGNAME is set from the passwd entry and cannot be overridden. HOME and
SHELL can.
- Every crontab file must end with a newline. Older crons silently drop the last
line without one, and crontab -e adds it for you.
Checking a schedule before trusting it
crontab -T myfile # cronie only, validate syntax without installing crontab -l # what is actually installed systemd-analyze calendar 'Mon *-*-* 09:00:00' # if you use timers instead
Cron itself has no dry-run and no "when does this next fire". Two habits cover that gap: keep the crontab in version control and install it from the file, and sanity check an expression against a schedule explainer such as crontab.guru before shipping it.
A field that can never match, like 0 0 31 2 *, is accepted without complaint
and simply never runs. Cron validates ranges, not calendars.