Engineering·Scheduling
Cron expressions explained, including the trap
Five fields, a handful of operators, and one rule that makes cron fire far more often than people expect. Day-of-month and day-of-week are combined with OR, not AND.
Before you start
- Shell access to something running cron, or a scheduler that uses the same syntax
Cron syntax is small enough to learn in ten minutes and contains exactly one rule that will surprise you. Most guides cover the five fields and stop before the surprise, which is why so many scheduled jobs run more often than anyone intended.
The five fields #
* * * * *
│ │ │ │ └── day of week (0-6, Sunday is 0)
│ │ │ └───── month (1-12)
│ │ └──────── day of month (1-31)
│ └─────────── hour (0-23)
└────────────── minute (0-59)
Left to right: minute, hour, day of month, month, day of week. If you remember nothing else, remember that the smallest unit is first.
Month and day names work too, so JAN and MON are valid, and 7 is accepted as another spelling of Sunday alongside 0.
The operators #
Four, and they compose:
| Operator | Example | Means |
|---|---|---|
* |
* |
Every value |
, |
1,15 |
These values |
- |
9-17 |
This range, inclusive |
/ |
*/15 |
Every nth, stepping from the start of the range |
The one worth pausing on is the step. */15 in the minute field fires at :00, :15, :30 and :45, which is what everyone expects. But a step counts from the beginning of the field's range, not from now. So */15 in the hour field means hours 0, 15 — and nothing else, because 30 and 45 are not valid hours. Likewise */4 in the hour field is 0, 4, 8, 12, 16, 20.
Steps also work on ranges. 9-17/4 is 9, 13, 17.
Some schedules that come up constantly #
*/15 * * * * every 15 minutes
0 * * * * hourly, on the hour
0 3 * * * daily at 03:00
30 9 * * 1-5 weekdays at 09:30
0 0 * * 0 weekly, Sunday midnight
0 0 1 * * monthly, on the 1st
0 0 1 1 * yearly, 1 January
The trap #
Here is the rule almost nobody is told.
When both day-of-month and day-of-week are restricted, cron runs the job when either matches. Not both. It is an OR, not an AND.
So this expression:
0 0 13 * 5
is not "midnight on Friday the 13th". It is "midnight on every 13th of the month, and midnight on every Friday". Instead of one or two runs a year, you get roughly sixty.
Watch out
This is the single most expensive cron misunderstanding, because the job still works. It just runs far more often than intended, which is easy to miss until it shows up as a bill, a rate limit, or duplicate emails.
The rule only applies when both fields are restricted. If either is *, the behaviour is what you would guess:
30 9 * * 1-5 weekdays only. Day-of-month is *, so no ambiguity.
0 0 1 * * the 1st only. Day-of-week is *, so no ambiguity.
0 0 13 * 5 every 13th OR every Friday. Both restricted.
There is no way to express "Friday the 13th" in standard cron. You schedule it for every Friday and check the date in your own code:
# Every Friday. The script decides whether it is actually the 13th.
0 0 * * 5 /usr/local/bin/only-if-thirteenth
#!/usr/bin/env bash
[ "$(date +%d)" = "13" ] || exit 0
exec /usr/local/bin/the-real-job
Which timezone #
Whatever the machine is set to, which is very often UTC on a server and local time on a laptop. That difference is a reliable source of "why did the nightly job run at 2am".
Cron itself has no timezone field. Modern cron implementations read the CRON_TZ variable at the top of a crontab, and systemd timers take OnCalendar with an explicit zone, but do not assume either is available. If a schedule matters, set the machine's timezone deliberately and write it down.
Daylight saving is worse. On the day clocks go forward, a job scheduled for 02:30 local does not run, because that time does not exist. On the day they go back, it can run twice. If a job must run exactly once, schedule it in UTC or make it idempotent.
What cron will not do #
The five-field format is a lower bound, not a standard everyone shares:
- No seconds. Six-field variants that add a seconds column belong to particular schedulers, such as Quartz or some language-level libraries, not to cron.
@reboot,L,W,#. Extensions from specific implementations.@rebootis common in Vixie cron;Lfor "last day of month" is a Quartz thing. Portable crontabs use none of them.- No overlap protection. If a job takes twelve minutes and is scheduled every five, you will have three copies running. Cron does not care. Use a lock file, or
flock.
# flock exits immediately rather than queueing, so runs cannot pile up.
*/5 * * * * /usr/bin/flock -n /tmp/job.lock /usr/local/bin/job
Check before you deploy #
The reliable way to understand a cron expression is to see the times it actually produces, rather than reasoning about the fields.
The cron expression generator here explains any expression in plain English, lists the next ten times it will fire, and warns you explicitly when both day fields are restricted, so the OR trap cannot bite silently. It runs in your browser with nothing uploaded.