By the Withuse team · Updated
The OR rule, which almost nobody expects
Cron has two ways to name a day — day of month and day of week — and the natural reading is that both must match. They do not. When each of those fields is something other than *, cron fires on a day matching either one.
0 0 1 * MON
expected : midnight on the 1st, but only when it is a Monday
actual : midnight every Monday, AND midnight on the 1st
-> about 5 runs a month instead of roughly 1 in 7The rule is in the crontab specification, and it exists so that "the 1st and every Monday" is expressible at all. It only applies when both fields are restricted: leave either one as * and the other becomes the sole constraint, which is the behaviour people assume they are getting. If you genuinely need the intersection, restrict one field in cron and test the other inside the script — there is no expression for it.
Sunday is 0 — and also 7 — and sometimes 1
The day-of-week field accepts 0 through 7, where both 0 and 7 mean Sunday. That redundancy exists so schedules written against either convention keep working, and within Unix cron it is harmless.
The hazard is crossing into Quartz, the scheduler behind Java's Quartz library, Spring's @Scheduled and several cloud services. Quartz numbers the field 1 through 7 with Sunday as 1, so every digit denotes a different day than it does here — a 5 is Friday in Unix cron and Thursday in Quartz. Nothing errors; the job simply runs on the wrong day. Writing SUN, MON, FRI instead sidesteps the whole question and survives review better.
Five fields, or you are not in cron
┌─ 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 = Sunday) │ │ │ │ │ 0 9 * * MON-FRI
Six or seven fields is Quartz syntax, which puts seconds first and may add a year at the end. Pasting one into a Unix crontab shifts every field one position left, so the expression still parses and runs at an entirely different time. The parser above refuses anything that is not five fields, because a silent misread costs far more than a rejection.
Timezones and the twice-yearly surprise
Cron runs in the system's local timezone unless the crontab sets CRON_TZ. In any zone that observes daylight saving, this makes two schedules a year ambiguous: when clocks jump forward the skipped hour never arrives, so a job scheduled inside it does not run; when they fall back, the repeated hour can run a job twice. Neither case produces an error or a log entry saying what happened.
The usual defences are to run servers on UTC and convert at the edges, or to avoid scheduling anything between 01:00 and 03:00 local. The tool above computes in UTC on purpose and says so, so you apply your own offset rather than trusting an assumption you cannot see.
Frequently asked questions
Why does my cron job run on days I did not schedule?
Almost always because both day fields are set, and cron combines them with OR rather than AND. If day-of-month and day-of-week are each anything other than *, the job fires on a matching day of the month or a matching weekday. We verified it: 0 0 1 * MON does not mean "the first of the month, if it is a Monday" — it fires every Monday and also on the first, which in a typical month is five runs instead of one. Only when one of the two fields is * does the other act as the sole constraint. To mean the intersection, restrict one field in cron and check the other inside your script.
Is Sunday 0 or 7 in cron?
In Unix cron it is both — the day-of-week field runs 0 to 7 with 0 and 7 alike meaning Sunday, so 0 0 * * 0 and 0 0 * * 7 schedule identical runs. We confirmed the two produce the same times. The trouble starts elsewhere: Quartz, used by Java schedulers and by several cloud providers, numbers the field 1 to 7 with Sunday as 1, so the same digit denotes a different day. A 5 means Friday in Unix cron and Thursday in Quartz. Writing the three-letter names instead — SUN, MON, FRI — removes the ambiguity everywhere and reads better in review. Nothing errors when the convention is wrong; the job simply runs on a different day than the author intended, which is why these bugs survive so long.
What timezone does cron use?
The system's local timezone, not UTC, unless the crontab sets CRON_TZ or the platform documents otherwise. That makes a schedule ambiguous twice a year in any zone observing daylight saving. When clocks jump forward, times inside the skipped hour never occur and jobs scheduled there do not run at all; when clocks fall back, the repeated hour can run a job twice. Standard advice is to run servers on UTC and convert at the edges, or to schedule outside the 01:00-03:00 window where transitions happen. The tool above computes in UTC deliberately, so you add your own offset rather than guessing which zone it assumed. Neither daylight-saving case produces an error or a log line explaining what happened, so a job that quietly skipped a night looks identical to one that was never scheduled.
What does the fifth field mean, and why do some expressions have six?
Standard Unix cron takes exactly five fields: minute, hour, day of month, month, day of week. Six or seven fields means you are looking at Quartz syntax, which prepends seconds and can append a year — it is what Java's Quartz scheduler, Spring's @Scheduled and several AWS services expect. Pasting a Quartz expression into a Unix crontab shifts every field by one position, so 0 0 12 * * ? becomes a schedule at a completely different time rather than an error. The parser above rejects anything that is not five fields and says so, because a silent misread is worse than a refusal. Quartz also uses ? in a day field to mean "no specific value", which Unix cron does not accept at all — another quick way to tell the two dialects apart at a glance.
How do step values like */15 actually work?
A step divides a range, so */15 in the minute field means "every 15th minute starting at 0", giving 0, 15, 30 and 45. The subtlety is that the step applies to the range in front of the slash, not to the clock. Writing 10-40/15 gives 10, 25 and 40, and a bare number with a step such as 5/10 runs from 5 to the end of the field, giving 5, 15, 25 and so on. This is also why */7 in the minute field is uneven across the hour boundary: it yields 0, 7, 14 up to 56, and then restarts at 0 four minutes later rather than continuing the cycle.
References
Guides
More browser-only tools at withuse.io/tools.