Cron day of week
In Unix cron, 0 and 7 both mean Sunday — but Quartz numbers Sunday as 1. And if you set the day-of-month field as well, cron combines them with OR, not AND.
By the Withuse team · Updated
Sunday has two numbers
0 0 * * 0 ┐
├─ identical schedules
0 0 * * 7 ┘
0=Sun 1=Mon 2=Tue 3=Wed 4=Thu 5=Fri 6=Sat 7=SunWe checked that both expressions produce exactly the same run times. The duplication is deliberate: it lets schedules written against either convention keep working, and inside Unix cron it causes no trouble at all.
The trouble starts at the boundary with Quartz — the scheduler behind Java's Quartz library, Spring's @Scheduled and several cloud services — which numbers the field 1 through 7 with Sunday as 1. Every digit therefore shifts by one: a 5 is Friday in Unix cron and Thursday in Quartz. Nothing errors. The job simply runs on the wrong day, which is why these bugs survive so long.
Writing SUN, MON, FRI instead removes the question entirely. Names are accepted by every modern cron, are case-insensitive, and mean the same day regardless of the numbering underneath.
The OR rule
This is the one that costs real money. Cron offers two ways to name a day, and when both are restricted it fires on either:
0 0 1 * MON
expected : midnight on the 1st, but only if it is a Monday
actual : midnight every Monday, AND midnight on the 1st
-> about 5 runs a month instead of roughly 1 in 7The behaviour is specified rather than a quirk: it exists so that "the 1st and every Monday" can be written at all. It applies only when both fields are restricted — leave either as * and the other becomes the sole constraint, which is what people expect by default. Our parser warns whenever an expression falls into the OR case, because the run list is otherwise the only way to notice.
Expressing the intersection
There is no five-field expression for "the first Monday of the month". The portable approach is to over-schedule in cron and narrow inside the job:
0 0 1-7 * MON # fires on days 1-7 OR any Monday #!/bin/sh # ...then bail out unless today is genuinely the first Monday [ "$(date +%-d)" -le 7 ] && [ "$(date +%u)" -eq 1 ] || exit 0
Quartz does provide extensions for this — # for the nth weekday and L for the last — but they are not portable to Unix cron and will be rejected outright. Keeping the narrowing in the script means the schedule stays readable and the logic works wherever it runs.
Weekday schedules and timezones
The day-of-week field is evaluated in the same zone as everything else — the system's local timezone, unless CRON_TZ says otherwise. That matters more for weekday schedules than for hourly ones, because a job near midnight can land on a different weekday depending on the offset: a Monday 00:30 job on a UTC server runs on Sunday evening for anyone in the Americas.
Daylight saving adds the familiar two exceptions a year. A run scheduled inside the hour that gets skipped never happens; one inside the hour that repeats can happen twice. Neither produces an error or a log line saying so, which is why scheduling outside the 01:00–03:00 local window is common advice.
Frequently asked questions
Is Sunday 0 or 7 in crontab?
Both, in Unix cron. The day-of-week field accepts 0 through 7 and treats 0 and 7 alike as Sunday, so 0 0 * * 0 and 0 0 * * 7 schedule exactly the same runs — we confirmed the two produce identical times. The redundancy exists so that schedules written against either convention keep working. Where it stops being harmless is outside Unix cron: Quartz, used by Java schedulers and several cloud services, numbers the field 1 through 7 with Sunday as 1. The same digit therefore means a different day, and a 5 that meant Friday becomes Thursday. Nothing errors when the convention is wrong, so the job simply runs on a day nobody chose, which is why these bugs survive for months.
Why does my weekly cron job run several times a month?
Because the day-of-month field is also set, and cron combines the two day fields with OR rather than AND. If both are anything other than *, the job fires on a matching day of the month or a matching weekday. We verified this: 0 0 1 * MON does not mean the first of the month when it happens to be a Monday — it fires every Monday and also on the first, which is roughly five runs a month instead of one. Only when one of the two fields is left as * does the other act as the sole constraint, which is the behaviour most people assume they are getting.
How do I run a job only on the first Monday of the month?
Not with a five-field cron expression alone, because the OR rule makes the intersection inexpressible. The standard approach is to restrict one field in cron and test the other inside the job: schedule 0 0 1-7 * MON, which fires on every Monday and on days 1 to 7, then have the script exit immediately unless the date is genuinely within the first week. Some implementations offer extensions — Quartz has # for nth-weekday and L for last — but they are not portable. Doing the check in the script keeps the schedule readable and works everywhere. It also fails safely: if the guard is wrong the job exits early rather than running on days you never intended.
Can I write MON-FRI instead of 1-5?
Yes, and you should. Every modern cron accepts three-letter English abbreviations for both the day-of-week and month fields — SUN, MON through SAT, and JAN through DEC — and they are case-insensitive. We confirmed 0 9 * * MON-FRI produces the expected weekday runs. Names remove the whole 0-versus-1 ambiguity, since SUN means Sunday regardless of which numbering the scheduler uses internally, and they survive code review far better than a bare digit whose meaning depends on the platform. The one limitation is that names cannot be used with step values in some older implementations, so prefer plain ranges such as MON-FRI over constructions like MON/2. Month names work the same way in the fourth field.
Does the day-of-week field respect timezones?
It uses the same timezone as the rest of the schedule — the system's local zone unless the crontab sets CRON_TZ. That matters more for weekday schedules than for hourly ones, because a job scheduled near midnight can fall on a different weekday depending on the offset. A Monday 00:30 job on a server running UTC fires on Sunday evening for anyone in the Americas. Daylight-saving transitions add the usual two exceptions a year: a run inside the skipped hour never happens, and one inside the repeated hour can happen twice, with no error either way. Setting CRON_TZ at the top of the crontab pins the schedule to a named zone and removes the guesswork, which is worth doing on any server whose local time is not UTC.
References
Paste an expression into the cron parser to see its next five runs and whether it falls into the OR case. More browser-only tools at withuse.io/tools.