The schedule that silently runs twice, or not at all
Cron looks trivial: five fields, decades-old syntax, no shortage of prior art. That simplicity is exactly why two specific mistakes keep causing real incidents — both are specified behaviour, not bugs, and both pass a syntax check without a single complaint.
The day-of-month / day-of-week OR rule
When both the day-of-month field and the day-of-week field are restricted (neither is a wildcard), the POSIX crontab specification and the crontab(5) man page behind most Linux cron daemons agree on the same rule: the job runs whenever either field matches, not only when both do. 0 0 13 * 5 reads, in English, like “midnight on Friday the 13th”. It is not that. It runs at midnight on the 13th of every month and at midnight on every Friday — two independent conditions joined by OR, producing several extra runs a month that have nothing to do with the 13th at all.
This is the single most common cron misunderstanding, and the builder above warns about it directly: restrict both day fields and the notice above the run table (“both day-of-month and day-of-week are restricted, so cron treats them as OR”) appears automatically. There is no way to express “the 13th, but only if it's a Friday” in a single standard cron line — the syntax has no AND for those two fields specifically. The usual workaround is to leave one field as a wildcard and test the other condition inside the job itself.
Daylight saving time
Cron evaluates its fields against the wall clock of whatever machine the daemon runs on, in that machine's configured time zone. Twice a year, in any zone that observes daylight saving, the wall clock is not monotonic: in spring an hour of local time is skipped entirely, and in autumn an hour repeats. A job scheduled inside the skipped hour may simply never fire that day; a job scheduled inside the repeated hour may fire twice, just under twenty-four hours apart. Which of those happens, and whether the daemon even notices, depends on the specific cron implementation — some handle it, plenty do not.
This is why the run-time table above always shows two columns, local time and UTC, rather than one. Local time is what a person reads off a clock; UTC is what most production schedulers and CI systems (GitHub Actions and most managed cron services among them) actually run on, precisely because UTC never observes daylight saving and so never has a skipped or repeated hour. If a job's correctness depends on running exactly once, at exactly the interval you expect, schedule it in UTC and do any local-time conversion in the application rather than in the crontab.
The schedule that never runs at all
A third failure mode is quieter than either of the above: an expression that is syntactically valid but can never match a real calendar date, such as 0 0 31 4 * (April has no 31st) or 0 0 30 2 * (February never has a 30th). Cron accepts the line without complaint and simply never runs the job — there is no error to notice, because none is raised. The OR rule can produce a milder version of the same trap: 0 0 30 2 5 looks broken but is not, because the day-of-week half of the OR keeps it firing every Friday even though the day-of-month half never matches. The next-run preview above exists specifically to surface this class of mistake before it reaches a real crontab: an expression returning zero results, or far fewer than five, is telling you something the syntax itself will not.
Read the specification yourself
- POSIX / The Open Group — crontab utility specification Defines the standard five-field syntax and the day-of-month/day-of-week OR rule.
- crontab(5) man page The Vixie cron behaviour this tool follows, including the OR rule and field ranges.
How it works
A standard cron expression is five fields separated by whitespace. Read left to right, they answer: at which minute, in which hour, on which day of the month, in which month, and on which day of the week should this job run — the syntax defined by the POSIX crontab specification.
* * * * *
| | | | |
| | | | +-- day of week (0-7, 0 and 7 both mean Sunday)
| | | +----- month (1-12 or JAN-DEC)
| | +-------- day of month (1-31)
| +----------- hour (0-23)
+-------------- minute (0-59)
A job runs at a given minute only when every field matches that moment — with one famous
exception covered below. The fields are not a countdown or an interval; they are a filter applied to the
clock. That is why */20 * * * * fires at :00, :20 and :40 of every hour rather than
"every 20 minutes starting whenever you saved the crontab".
The five fields and their allowed values
| Position | Field | Allowed values | Notes |
|---|---|---|---|
| 1 | Minute | 0-59 | Smallest unit standard cron understands. |
| 2 | Hour | 0-23 | 24-hour clock. Midnight is 0, not 24. |
| 3 | Day of month | 1-31 | Days that do not exist in a month are simply skipped. |
| 4 | Month | 1-12 | Names also work: JAN … DEC. |
| 5 | Day of week | 0-7 | 0 and 7 are both Sunday. Names work: SUN … SAT. |
Operators you can use in any field
| Operator | Example | Meaning |
|---|---|---|
| * | * * * * * | Every value of that field. |
| , | 0,15,30,45 | A list of specific values. |
| - | 9-17 | An inclusive range. |
| / | */5 | A step across the whole field: 0, 5, 10, … |
| - | 10-30/5 | A step across a range: 10, 15, 20, 25, 30. |
| @ | @daily | Shorthand for 0 0 * * *. Also @hourly, @weekly, @monthly, @yearly. |
How the next run times are calculated
This tool does not guess. It parses each field into the exact set of values it matches, then walks forward one calendar day at a time from the current minute, testing the month and day rules for each day and expanding the matching hours and minutes in order. That approach handles leap years, months of different lengths and unsatisfiable expressions correctly.
The search stops after roughly four years. If an expression such as 0 0 30 2 *
(30 February) can never match, the tool reports that there are no upcoming runs instead of freezing.
Local times are rendered using your device time zone; UTC is shown alongside because most servers and
most hosted schedulers run on UTC.
Worked examples
1. Quarter-hourly during office hours on weekdays
*/15 9-17 * * 1-5
Minute steps by 15 (:00, :15, :30, :45), the hour is restricted to the range 9 through 17, and the day of week is Monday through Friday. That is 36 runs per working day.
The trap here is the hour range. 9-17 includes hour 17 in its entirety, so the
last run of the day is 17:45, not 17:00. If you want the final run to be exactly 17:00, use
0 17 * * 1-5 as a separate line, or narrow the range to 9-16 and accept that
nothing fires after 16:45.
2. Monthly versus quarterly
0 0 1 * * runs at midnight on the 1st of every month. 0 0 1 */3 * runs at midnight on the 1st of every third month.
Steps in the month field are anchored to the start of the field, not to today. */3
expands to months 1, 4, 7 and 10 — January, April, July and October — regardless of when you
install the crontab. If your fiscal quarters start in February you need 2,5,8,11 instead.
The same anchoring applies everywhere: */7 in the day-of-month field means days 1, 8, 15,
22 and 29, which is not a stable weekly cadence because the count restarts each month.
3. The day-of-month / day-of-week OR rule
0 12 13 * 5
Read literally this looks like "noon on the 13th, but only if it is a Friday". It is not. When both the day-of-month field and the day-of-week field are restricted, cron combines them with OR: the job runs at noon on the 13th of any month and on every Friday. In a typical year that is more than sixty runs, not one or two.
To actually get "Friday the 13th only" you cannot use a single standard cron expression. The usual
workaround is to schedule 0 12 13 * * and start the command with a guard such as
[ "$(date +\%u)" = 5 ] && /path/to/job. Note the escaped percent sign: in a crontab
an unescaped % is turned into a newline and everything after it is fed to the command
on standard input.
4. The step-wraparound trap
*/45 * * * *
This does not run every 45 minutes. The minute field only counts from 0 to 59, so */45
expands to minutes 0 and 45. The job fires at 00:00 and 00:45, then again at 01:00 — a gap of
fifteen minutes, not forty-five.
Any interval that does not divide evenly into 60 minutes (or into 24 hours) will behave like this.
For a genuine 90-minute cadence you need to enumerate the times yourself, for example
0 0,3,6,9,12,15,18,21 * * * combined with 30 1,4,7,10,13,16,19,22 * * *,
or move the interval logic into the job itself.
5. A nightly job and daylight saving time
0 2 * * *
On a server set to UTC this is unremarkable: one run per day, 24 hours apart, forever. On a server set to a zone that observes daylight saving time, 02:00 is exactly the hour that disappears in spring and repeats in autumn. Depending on the cron implementation the job may be skipped on the spring transition day, or run twice on the autumn one.
The reliable fix is to run schedulers in UTC and convert at the edges, or to pick an hour that no common time zone shifts across, such as 04:00 or 05:00. Vixie cron on Linux has special handling for jobs in the skipped hour; container schedulers and cloud cron services usually do not.
Frequently asked questions
What do the five fields actually mean?
In order: minute (0–59), hour (0–23), day of month (1–31), month (1–12) and day of week (0–7). A run happens at any minute where all the fields are satisfied. There is no seconds field in standard cron and no year field — those belong to Quartz and Spring, which use six or seven fields.
If I restrict both day-of-month and day-of-week, are they AND-ed or OR-ed?
OR-ed. This is the single most misunderstood rule in cron. When either of the two day fields
is *, the other one simply filters the days as you would expect. But when
neither is *, the daemon runs the job on any day that matches the day-of-month
field or the day-of-week field.
So 0 0 1 * 1 is not "the 1st, if it is a Monday". It is "the 1st of every month, plus
every Monday". If you need both conditions to hold, use a wildcard in one field and test the other
condition inside the command.
One subtlety: a field beginning with * counts as unrestricted for the purposes of this
rule, including */2. That matches the behaviour of Vixie cron, the implementation behind
cron on most Linux distributions, and this tool follows it, per the rule documented in the
crontab(5) man page.
Is Sunday 0 or 7?
Both. Standard cron accepts 0 through 7 in the day-of-week field and treats 0
and 7 as Sunday, which makes ranges such as 5-7 (Friday, Saturday, Sunday) expressible.
Monday is always 1. Some non-cron schedulers — notably Quartz — use 1 for Sunday and 7 for
Saturday, so double-check before copying an expression between systems.
What is the difference between 5 * * * * and */5 * * * *?
5 * * * * runs once an hour, at five minutes past the hour —
24 runs a day. */5 * * * * runs every five minutes, at :00, :05, :10 and so on —
288 runs a day. The slash introduces a step; a bare number is a single value. Mixing them up is the
most common cause of a job that runs 12 times more often than intended.
Which time zone does cron use?
The system time zone of the machine running the daemon, unless the crontab sets
CRON_TZ (Vixie cron) or TZ at the top of the file. Managed schedulers vary:
GitHub Actions schedule triggers are always UTC, Kubernetes CronJob defaults to the
controller time zone but accepts a timeZone field, and most cloud schedulers let you pick
a zone explicitly. The tool above shows both your local time and UTC so you can check which one your
expression really targets.
What happens to my job during a daylight saving change?
It depends on the implementation. Vixie cron treats jobs scheduled in the hour
that is skipped in spring as due immediately after the jump, and suppresses duplicate runs in the
repeated autumn hour for fixed-time jobs. Interval jobs such as */10 * * * * simply follow
the wall clock, so you get one short hour and one long one. Container and cloud schedulers frequently
have no special handling at all. Running everything in UTC removes the problem entirely.
Can I schedule something every 30 seconds?
Not with standard five-field cron — one minute is the finest granularity.
The classic workaround is a single crontab line that runs the command, sleeps and runs it again:
* * * * * /path/to/job; sleep 30; /path/to/job. Cleaner alternatives are systemd timers
with OnUnitActiveSec=30s, or a long-running process with its own internal loop. Quartz and
Spring cron expressions do have a seconds field, but they are not compatible with system cron.
Why does my cron job never run?
In rough order of likelihood:
- Environment. Cron runs with a minimal environment and a short
PATH(often just/usr/bin:/bin). Use absolute paths for every binary and every file. - Unescaped percent signs. In a crontab,
%becomes a newline. Anydate +%Ymust be writtendate +\%Y. - No trailing newline. Some cron implementations silently ignore the last line of a crontab file if it does not end with a newline.
- Wrong crontab. A job added with
crontab -eas your user is not the same as/etc/crontabor a file in/etc/cron.d, and the system files need an extra user column. - Output going nowhere. Redirect to a file
(
>> /var/log/myjob.log 2>&1) so failures are visible, then checkjournalctl -u cronor/var/log/syslog.
Does this tool support Quartz, Spring or six-field expressions?
No. It deliberately implements standard five-field Vixie/POSIX cron, which is
what Linux crontabs, Kubernetes CronJob, GitHub Actions and most cloud schedulers use. If you paste a
six- or seven-field expression, or one containing Quartz specials such as L,
W or #, you will get an explicit error rather than a wrong answer. The
Quartz-style ? placeholder is accepted and treated as *, since that is what
it means in practice.
What does "no upcoming runs" mean?
The expression is syntactically valid but can never match a real date within the
four-year search window. The classic case is 0 0 30 2 * — there is no 30 February.
0 0 31 4 * is another: April has 30 days. Note that a valid but rare expression such as
0 0 29 2 * (29 February) does have runs, just infrequent ones, so you may see fewer than
five results rather than none.
Are my expressions sent anywhere?
No. The parser, the explainer and the next-run calculation are all plain JavaScript running in your browser. There is no API call and no server-side processing, so the expression you paste in is never uploaded or logged anywhere. You can save the page and use it offline, and the tool will keep working.
How do I write a schedule for the last day of the month?
Standard cron cannot express it — there is no L token. The
usual pattern is to run the job on the 28th through the 31st and let the command decide:
0 3 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /path/to/job. The guard is
true only on whichever of those days is actually the last one. Quartz users can write
0 0 3 L * ? instead.
Use cases and limitations
What people schedule with cron
- Backups and snapshots. Nightly database dumps, weekly full backups, monthly archive rotation. Usually run in the small hours in the server time zone.
- Certificate renewal. ACME clients typically want a twice-daily check at a random minute so that renewals do not stampede the certificate authority on the hour.
- Cache warming and index rebuilds. Frequent short jobs, often
*/5or*/15, restricted to business hours to save resources overnight. - Report generation and billing runs. Monthly or quarterly schedules pinned to a
specific day of the month, such as
0 6 1 * *. - CI and repository automation. GitHub Actions, GitLab CI and most build systems
accept five-field cron in a
scheduleblock, always interpreted as UTC. - Kubernetes CronJob. Same syntax, plus a
timeZonefield and concurrency policies that plain cron does not have.
What cron cannot do
- Sub-minute scheduling. One minute is the floor.
- Relative intervals. "Every 90 minutes" and "every 45 minutes" cannot be expressed because steps are anchored to the start of the field and reset at its boundary.
- Last day of month, nth weekday, business-day arithmetic. These require Quartz syntax or a guard inside the command.
- Catch-up after downtime. If the machine is asleep or the daemon is stopped, the
run is simply missed.
anacronexists for exactly this reason on laptops and workstations. - Overlap protection. If a run takes longer than the interval, cron happily starts
another one. Wrap long jobs in
flockor an equivalent lock.
Limitations of this tool
The run times shown here are a simulation of what a correct cron implementation would do, computed against your browser clock and time zone database. They are a planning aid, not a guarantee: your daemon's time zone, its daylight-saving behaviour, missed runs during downtime and any scheduler-specific extensions can all shift reality. The search window is capped at about four years, so very sparse schedules may return fewer than five results. Quartz, Spring and other non-standard dialects are intentionally out of scope.