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.
| Nickname | Equivalent | Meaning |
|---|---|---|
@yearly | 0 0 1 1 * | midnight, January 1 |
@annually | 0 0 1 1 * | same as @yearly |
@monthly | 0 0 1 * * | midnight, first of the month |
@weekly | 0 0 * * 0 | midnight, Sunday |
@daily | 0 0 * * * | midnight |
@midnight | 0 0 * * * | same as @daily |
@hourly | 0 * * * * | 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:
| Implementation | Nicknames | @reboot | @midnight |
|---|---|---|---|
| Vixie cron, Debian and Ubuntu | yes | yes | yes |
| cronie, Fedora, RHEL, Arch | yes | yes | yes, undocumented |
| BSD and macOS cron | yes | yes | yes |
| busybox crond | yes | yes | yes, if built with special times |
POSIX crontab | no | no | no |
@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.
| Expression | Field | Fires at |
|---|---|---|
*/15 | minute | 0, 15, 30, 45 |
0-30/10 | minute | 0, 10, 20, 30 |
5-59/10 | minute | 5, 15, 25, 35, 45, 55 |
*/2 | hour | 0, 2, 4, … 22 |
1-23/2 | hour | 1, 3, 5, … 23 |
*/3 | day of month | 1, 4, 7, … 31 |
*/2 | day of week | 0, 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=.