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
FieldRangeNotes
minute0-59—
hour0-2324-hour clock, no AM/PM, no 24
day of month1-31a 31 in a 30-day month simply never fires
month1-12or jan feb … dec, case insensitive
day of week0-7or 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

OperatorMeaningExample
*every value in the range* * * * * every minute
,a list0,15,30,45
-an inclusive range9-17 is 9 through 17
/a step through a range*/5, 0-30/10
namesmonth and weekday onlyjan, 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 monthDay of weekBehavior
**every day
15*the 15th only
*5Fridays only
155the 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=UTC

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