DEV Community

delay.run
delay.run

Posted on

Cron expression for every Monday at 9am (and other weekly schedules)

Short answer: the cron expression for every Monday at 9:00 am is:

0 9 * * 1
Enter fullscreen mode Exit fullscreen mode

That's minute 0, hour 9, any day of the month, any month, day of week 1 (Monday). The rest of this post covers the variations people usually need next, and a few traps in the day-of-week field that make jobs run on the wrong day.

Reading the five fields

┌──────── 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 are both Sunday)
│ │ │ │ │
0 9 * * 1
Enter fullscreen mode Exit fullscreen mode

Hours use a 24-hour clock, so 9 pm is 21. Most cron implementations accept three-letter names, so 0 9 * * MON means the same thing and is easier to read in a crontab you'll revisit next year.

Weekly schedules you can copy

Schedule Expression
Every Monday at 9:00 0 9 * * 1
Every Monday at 9:30 30 9 * * 1
Every Monday at 9 am and 5 pm 0 9,17 * * 1
Every weekday (Mon-Fri) at 9:00 0 9 * * 1-5
Monday, Wednesday and Friday at 9:00 0 9 * * 1,3,5
Every Sunday at midnight 0 0 * * 0
Every Saturday and Sunday at 10:00 0 10 * * 6,0
Every 15 minutes on Mondays, 9 to 5 */15 9-16 * * 1
Every Monday at 9:00, January to June 0 9 * 1-6 1

Note the 9-to-5 example uses 9-16 for the hour. */15 9-17 would also fire at 17:00, 17:15, 17:30 and 17:45.

Trap 1: "first Monday of the month" doesn't work the obvious way

This looks right, but isn't:

0 9 1-7 * 1     # NOT "first Monday"
Enter fullscreen mode Exit fullscreen mode

When both day-of-month and day-of-week are restricted (neither is *), standard cron runs the job when either matches. So this runs on days 1 to 7 of every month and on every Monday. That's up to 11 runs a month instead of one.

The reliable fix is to schedule on days 1 to 7 and let the command check the weekday:

0 9 1-7 * * [ "$(date +\%u)" = 1 ] && /usr/local/bin/monthly-report.sh
Enter fullscreen mode Exit fullscreen mode

date +%u prints 1 for Monday through 7 for Sunday. The % has to be escaped as \% in a crontab, because cron treats an unescaped % as a newline. Forgetting that is a classic reason a job "never runs".

The same pattern works for "last Friday of the month":

0 9 22-31 * * [ "$(date +\%u)" = 5 ] && [ "$(date -d '+7 days' +\%m)" != "$(date +\%m)" ] && /path/to/job.sh
Enter fullscreen mode Exit fullscreen mode

The second test checks that a week from today falls in the next month, which is only true for the last Friday. date -d is GNU syntax; on macOS use date -v+7d.

Trap 2: Sunday is 0, and also 7

Most cron versions accept both 0 and 7 for Sunday. A range like 1-7 therefore means Monday to Sunday, every day. If you want the weekend, write 6,0 or SAT,SUN, not 6-7 unless you've checked your implementation supports it.

Trap 3: other schedulers use different formats

The five-field format above is standard Unix cron, also used by most hosted schedulers, GitHub Actions and Kubernetes CronJobs. Some systems differ:

Quartz and Spring use six or seven fields, starting with seconds, and need ? in one of the day fields:

0 0 9 ? * MON        # second minute hour day-of-month month day-of-week
Enter fullscreen mode Exit fullscreen mode

Also note that in Quartz, day-of-week runs 1 to 7 starting with Sunday, so 2 is Monday. Copying 0 9 * * 1 into Quartz-style config won't do what you expect.

AWS EventBridge uses six fields with a year at the end, and also requires ?:

cron(0 9 ? * MON *)
Enter fullscreen mode Exit fullscreen mode

If a scheduler rejects your expression, count the fields first. That's the cause most of the time.

Trap 4: 9 am in which time zone?

Cron runs in the time zone of the machine or service running it. On a cloud server that's very often UTC, so 0 9 * * 1 fires at 9:00 UTC, which may be 10:00 or 11:00 for your team in Europe, or the middle of the night in Asia.

Check what the server thinks the time is:

timedatectl          # on systemd-based Linux
date
Enter fullscreen mode Exit fullscreen mode

Either convert your schedule to UTC, or use a scheduler that lets you set a time zone per job. If you convert, remember that daylight saving time shifts the offset twice a year.

Test the schedule before you rely on it

A weekly job gives you one chance a week to notice it's wrong. Before saving it:

  1. Paste the expression into a cron expression explainer and read the next few run times it lists.
  2. Temporarily change it to run a few minutes from now, check it works, then set the real schedule.
  3. Log the output so a failure isn't silent:
0 9 * * 1 /usr/local/bin/weekly-report.sh >> /var/log/weekly-report.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Running it without a server

If the job is really "call this URL every Monday at 9", you don't need a machine with a crontab at all. A hosted scheduler like delay.run takes the same five-field expression, calls your endpoint on schedule, and logs each run with its status code and response, so you can see last Monday's result instead of guessing.

Whichever route you take, 0 9 * * 1 is the line you need. Just double-check the time zone and the day-of-month field, and it will fire once a week, exactly when you expect.

Top comments (0)