cron Cheatsheet

Special Strings and Steps

Use this cron reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

The @ nicknames

Instead of five fields, a line may start with one nickname. They are a Vixie cron extension, not POSIX.

NicknameEquivalentMeaning
@yearly0 0 1 1 *midnight, January 1
@annually0 0 1 1 *same as @yearly
@monthly0 0 1 * *midnight, first of the month
@weekly0 0 * * 0midnight, Sunday
@daily0 0 * * *midnight
@midnight0 0 * * *same as @daily
@hourly0 * * * *on the hour
@reboot—once, when cron starts
@daily  /usr/local/bin/rotate-logs.sh
@reboot /usr/local/bin/start-tunnel.sh

Support, since this matters more than the syntax:

ImplementationNicknames@reboot@midnight
Vixie cron, Debian and Ubuntuyesyesyes
cronie, Fedora, RHEL, Archyesyesyes, undocumented
BSD and macOS cronyesyesyes
busybox crondyesyesyes, if built with special times
POSIX crontabnonono

@reboot and why it may not fire

@reboot runs when the cron daemon starts, which is not the same as when the machine finished booting.

- On a system where cron starts early, the network, mounts, and other services may not be up yet. Sleep first or, better, express the real dependency with a systemd unit. - Restarting the cron service re-triggers every @reboot line. Reloading the crontab does not. - @reboot in a user crontab still runs, but with no login session, so desktop, keyring, and D-Bus access will not work. - It does not exist at all in POSIX cron or in some containers where cron never runs as PID 1's child.

@reboot sleep 60 && /usr/local/bin/start-tunnel.sh

For anything that must run after a specific service, a systemd unit with After=network-online.target is the correct tool, not @reboot.

How steps really work

A step is range/N, and it always means "starting at the low end of the range, every Nth value, stopping at the high end". * is just the shorthand for the full range.

ExpressionFieldFires at
*/15minute0, 15, 30, 45
0-30/10minute0, 10, 20, 30
5-59/10minute5, 15, 25, 35, 45, 55
*/2hour0, 2, 4, … 22
1-23/2hour1, 3, 5, … 23
*/3day of month1, 4, 7, … 31
*/2day of week0, 2, 4, 6, which is Sun, Tue, Thu, Sat

Two consequences people get wrong:

- A step does not mean "every N minutes". */35 in the minute field fires at :00 and :35, so the gaps are 35 minutes and then 25 minutes. Cron restarts the pattern every hour, because each field is independent. - A step in the day-of-month field restarts every month. */3 fires on the 1st, 4th, … 31st, then the 1st again, so the interval across a month boundary is not 3 days.

Intervals that do not divide their field evenly cannot be expressed in cron. Every 90 minutes, every 40 minutes, or every 5 days are all impossible as a single expression.

# Every 90 minutes: enumerate it, two lines for the odd and even hours
0 0,3,6,9,12,15,18,21   * * * /usr/local/bin/job.sh
30 1,4,7,10,13,16,19,22 * * * /usr/local/bin/job.sh

The other option is a modulo guard on a shorter schedule, which is what the cronie man page itself suggests:

* * * * * [ $(( $(date +\%s) / 60 \% 90 )) -eq 0 ] && /usr/local/bin/job.sh

Or use a systemd timer, where OnUnitActiveSec=90min says exactly that.

Sub-minute schedules

Cron's resolution is one minute, full stop. For anything faster:

# a self-sleeping loop, the traditional hack
* * * * * /usr/local/bin/job.sh
* * * * * sleep 20 && /usr/local/bin/job.sh
* * * * * sleep 40 && /usr/local/bin/job.sh

That works, but the runs drift and overlap. A systemd timer with OnUnitActiveSec=20s, or a long-running process with its own loop, is the honest answer.

Time zones, DST, and the two lost hours

Cron uses the daemon's local time zone. That makes the spring-forward and fall-back transitions genuinely dangerous for jobs scheduled between 01:00 and 03:00.

What Vixie cron and Debian's cron actually do, per man 8 cron, for a clock change of less than three hours:

- Fixed-time jobs, meaning no * in the hour or minute field, are protected. If the clock moved forward, a job whose slot was skipped runs soon after the change. If it moved backward, a job in the repeated hour is not re-run. - Wildcard jobs, such as */10 * * * * or @hourly, follow the new clock immediately. That is what you want, and it means one interval is short or long on transition day. - A clock change of more than three hours is treated as a correction, and the new time is used immediately with no compensation. - macOS and BSD cron only do this when the daemon is started with -s, which is not the default, so a fixed-time job inside a skipped hour is simply lost.

Fixes, in order of reliability:

# 1. Run the whole crontab in UTC (cronie, Debian cron)
CRON_TZ=UTC
30 2 * * * /usr/local/bin/job.sh

# 2. Schedule outside 01:00-03:00 local
30 4 * * * /usr/local/bin/job.sh

# 3. Make the job idempotent and let it run twice

CRON_TZ is per-crontab and is read by cronie and Debian's cron. It is not in the BSD or macOS cron, where the only lever is the system time zone or the TZ variable exported inside the command. Log timestamps stay in the daemon's local zone either way.

RANDOM_DELAY=N (cronie) delays every job in the crontab by a random 0 to N minutes, which is how a fleet avoids all hitting a server at 0 3 * * *. The systemd equivalent is RandomizedDelaySec=.